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/README.RU.md b/README.RU.md
new file mode 100644
index 0000000..379091c
--- /dev/null
+++ b/README.RU.md
@@ -0,0 +1,818 @@
+`PhpZip`
+========
+`PhpZip` - php библиотека для продвинутой работы с ZIP-архивами.
+
+[](https://travis-ci.org/Ne-Lexa/php-zip)
+[](https://packagist.org/packages/nelexa/zip)
+[](https://packagist.org/packages/nelexa/zip)
+[](https://php.net/)
+[](https://packagist.org/packages/nelexa/zip)
+
+[English Documentation](README.md)
+
+Содержание
+----------
+- [Функционал](#Features)
+- [Требования](#Requirements)
+- [Установка](#Installation)
+- [Примеры](#Examples)
+- [Глоссарий](#Glossary)
+- [Документация](#Documentation)
+ + [Обзор методов класса `\PhpZip\ZipFile`](#Documentation-Overview)
+ + [Создание/Открытие ZIP-архива](#Documentation-Open-Zip-Archive)
+ + [Чтение записей из архива](#Documentation-Open-Zip-Entries)
+ + [Перебор записей/Итератор](#Documentation-Zip-Iterate)
+ + [Получение информации о записях](#Documentation-Zip-Info)
+ + [Добавление записей в архив](#Documentation-Add-Zip-Entries)
+ + [Удаление записей из архива](#Documentation-Remove-Zip-Entries)
+ + [Работа с записями и с архивом](#Documentation-Entries)
+ + [Работа с паролями](#Documentation-Password)
+ + [zipalign - выравнивание архива для оптимизации Android пакетов (APK)](#Documentation-ZipAlign-Usage)
+ + [Отмена изменений](#Documentation-Unchanged)
+ + [Сохранение файла или вывод в браузер](#Documentation-Save-Or-Output-Entries)
+ + [Закрытие архива](#Documentation-Close-Zip-Archive)
+- [Запуск тестов](#Running-Tests)
+- [Обновление версий](#Upgrade)
+ + [Обновление с версии 2 до версии 3.0](#Upgrade-v2-to-v3)
+
+### Функционал
+- Открытие и разархивирование ZIP-архивов.
+- Создание ZIP-архивов.
+- Модификация ZIP-архивов.
+- Чистый php (не требуется расширение `php-zip` и класс `\ZipArchive`).
+- Поддерживается сохранение архива в файл, вывод архива в браузер или вывод в виде строки, без сохранения в файл.
+- Поддерживаются комментарии архива и комментарии отдельных записей.
+- Получение подробной информации о каждой записи в архиве.
+- Поддерживаются только следующие методы сжатия:
+ + Без сжатия (Stored).
+ + Deflate сжатие.
+ + BZIP2 сжатие при наличии расширения `php-bz2`.
+- Поддержка `ZIP64` (размер файла более 4 GB или количество записей в архиве более 65535).
+- Встроенная поддержка выравнивания архива для оптимизации Android пакетов (APK) [`zipalign`](https://developer.android.com/studio/command-line/zipalign.html).
+- Работа с паролями для PHP 5.5
+> **Внимание!**
+>
+> Для 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`
+
+Последняя стабильная версия: [](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
+```
+### Обновление версий
+#### Обновление с версии 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..eed4546 100644
--- a/README.md
+++ b/README.md
@@ -1,133 +1,232 @@
`PhpZip`
========
-`PhpZip` - php library for manipulating zip archives.
+`PhpZip` is a php-library for extended work with ZIP-archives.
[](https://travis-ci.org/Ne-Lexa/php-zip)
[](https://packagist.org/packages/nelexa/zip)
[](https://packagist.org/packages/nelexa/zip)
-[](https://php.net/)
+[](https://php.net/)
[](https://packagist.org/packages/nelexa/zip)
+[Russian Documentation](README.RU.md)
+
Table of contents
-----------------
- [Features](#Features)
- [Requirements](#Requirements)
- [Installation](#Installation)
- [Examples](#Examples)
+- [Glossary](#Glossary)
- [Documentation](#Documentation)
- + [Open Zip Archive](#Documentation-Open-Zip-Archive)
- + [Get Zip Entries](#Documentation-Open-Zip-Entries)
- + [Add Zip Entries](#Documentation-Add-Zip-Entries)
- + [ZipAlign Usage](#Documentation-ZipAlign-Usage)
- + [Save Zip File or Output](#Documentation-Save-Or-Output-Entries)
- + [Close Zip Archive](#Documentation-Close-Zip-Archive)
-- [Running Tests](#Running-Tests)
-- [Upgrade version 2 to version 3](#Upgrade)
+ + [Overview of methods of the class `\PhpZip\ZipFile`](#Documentation-Overview)
+ + [Creation/Opening of ZIP-archive](#Documentation-Open-Zip-Archive)
+ + [Reading entries from the archive](#Documentation-Open-Zip-Entries)
+ + [Iterating entries](#Documentation-Zip-Iterate)
+ + [Getting information about entries](#Documentation-Zip-Info)
+ + [Adding entries to the archive](#Documentation-Add-Zip-Entries)
+ + [Deleting entries from the archive](#Documentation-Remove-Zip-Entries)
+ + [Working with entries and archive](#Documentation-Entries)
+ + [Working with passwords](#Documentation-Password)
+ + [zipalign - alignment tool for Android (APK) files](#Documentation-ZipAlign-Usage)
+ + [Undo changes](#Documentation-Unchanged)
+ + [Saving a file or output to a browser](#Documentation-Save-Or-Output-Entries)
+ + [Closing the archive](#Documentation-Close-Zip-Archive)
+- [Running the tests](#Running-Tests)
+- [Upgrade](#Upgrade)
+ + [Upgrade version 2 to version 3.0](#Upgrade-v2-to-v3)
### Features
- Opening and unzipping zip files.
-- Create zip files.
-- Update zip files.
+- Creating ZIP-archives.
+- Modifying ZIP archives.
- Pure php (not require extension `php-zip` and class `\ZipArchive`).
-- Output the modified archive as a string or output to the browser without saving the result to disk.
-- Support archive comment and entries comments.
-- Get info of zip entries.
-- Support zip password for PHP 5.5, include update and remove password.
-- Support encryption method `Traditional PKWARE Encryption (ZipCrypto)` and `WinZIP AES Encryption`.
-- Support `ZIP64` (size > 4 GiB or files > 65535 in a .ZIP archive).
-- Support archive alignment functional [`zipalign`](https://developer.android.com/studio/command-line/zipalign.html).
+- It supports saving the archive to a file, outputting the archive to the browser, or outputting it as a string without saving it to a file.
+- Archival comments and comments of individual entry are supported.
+- Get information about each entry in the archive.
+- Only the following compression methods are supported:
+ + No compressed (Stored).
+ + Deflate compression.
+ + BZIP2 compression with the extension `php-bz2`.
+- Support for `ZIP64` (file size is more than 4 GB or the number of entries in the archive is more than 65535).
+- Built-in support for aligning the archive to optimize Android packages (APK) [`zipalign`](https://developer.android.com/studio/command-line/zipalign.html).
+- Working with passwords for PHP 5.5
+> **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: [](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($filename);
```
-Open zip archive from data string.
+ **ZipFile::openFile** - opens a zip-archive from a file.
+```php
+$zipFile = new \PhpZip\ZipFile();
+$zipFile->openFile('file.zip');
+```
+ **ZipFile::openFromString** - opens a zip-archive from a string.
```php
$zipFile = new \PhpZip\ZipFile();
$zipFile->openFromString($stringContents);
```
-Open zip archive from stream resource.
+ **ZipFile::openFromStream** - opens a zip-archive from the stream.
```php
-$stream = fopen($filename, 'rb');
+$stream = fopen('file.zip', 'rb');
$zipFile = new \PhpZip\ZipFile();
$zipFile->openFromStream($stream);
```
-#### Get Zip Entries
-Get num entries.
+#### Reading entries from the archive
+ **ZipFile::count** - returns the number of entries in the archive.
```php
$count = count($zipFile);
// or
$count = $zipFile->count();
```
-Get list files.
+ **ZipFile::getListFiles** - returns list of archive files.
```php
$listFiles = $zipFile->getListFiles();
-// Example result:
-//
-// $listFiles = [
-// 'info.txt',
-// 'path/to/file.jpg',
-// 'another path/'
-// ];
+// example array contents:
+// array (
+// 0 => 'info.txt',
+// 1 => 'path/to/file.jpg',
+// 2 => 'another path/',
+// )
```
-Get entry contents.
+ **ZipFile::getEntryContent** - returns the entry contents using its name.
```php
// $entryName = 'path/to/example-entry-name.txt';
$contents = $zipFile[$entryName];
+// or
+$contents = $zipFile->getEntryContents($entryName);
```
-Checks whether a entry exists.
+ **ZipFile::hasEntry** - checks if there is an entry in the archive.
```php
// $entryName = 'path/to/example-entry-name.txt';
$hasEntry = isset($zipFile[$entryName]);
+// or
+$hasEntry = $zipFile->hasEntry($entryName);
```
-Check whether the directory entry.
+ **ZipFile::isDirectory** - checks that the entry in the archive is a directory.
```php
// $entryName = 'path/to/';
$isDirectory = $zipFile->isDirectory($entryName);
```
-Extract all files to directory.
+ **ZipFile::extractTo** - extract the archive contents.
+The directory must exist.
```php
$zipFile->extractTo($directory);
```
-Extract some files to directory.
+Extract some files to the directory.
+The directory must exist.
```php
$extractOnlyFiles = [
"filename1",
@@ -136,77 +235,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 +340,447 @@ print_r($zipAllInfo);
//
// ...
//)
-
```
-#### Add Zip Entries
-Adding a file to the zip-archive.
+#### Adding entries to the archive
+
+All methods of adding entries to a ZIP archive allow you to specify a method for compressing content.
+
+The following methods of compression are available:
+- `\PhpZip\ZipFile::METHOD_STORED` - no compression
+- `\PhpZip\ZipFile::METHOD_DEFLATED` - Deflate compression
+- `\PhpZip\ZipFile::METHOD_BZIP2` - Bzip2 compression with the extension `ext-bz2`
+
+ **ZipFile::addFile** - adds a file to a ZIP archive from the given path.
```php
-// entry name is file basename.
-$zipFile->addFile($filename);
-// or
-$zipFile->addFile($filename, null);
+// $file = '...../file.ext';
+$zipFile->addFile($file);
-// with entry name
-$zipFile->addFile($filename, $entryName);
+// you can specify the name of the entry in the archive (if null, then the last component from the file name is used)
+$zipFile->addFile($file, $entryName);
// or
-$zipFile[$entryName] = new \SplFileInfo($filename);
+$zipFile[$entryName] = new \SplFileInfo($file);
-// with compression method
-$zipFile->addFile($filename, $entryName, ZipFile::METHOD_DEFLATED); // Deflate compression
-$zipFile->addFile($filename, $entryName, ZipFile::METHOD_STORED); // No compression
-$zipFile->addFile($filename, null, ZipFile::METHOD_BZIP2); // BZIP2 compression
+// you can specify a compression method
+$zipFile->addFile($file, $entryName, ZipFile::METHOD_STORED); // No compression
+$zipFile->addFile($file, $entryName, ZipFile::METHOD_DEFLATED); // Deflate compression
+$zipFile->addFile($file, $entryName, ZipFile::METHOD_BZIP2); // BZIP2 compression
```
-Add entry from string data.
+ **ZipFile::addFromString** - adds a file to a ZIP archive using its contents.
```php
-$zipFile[$entryName] = $data;
+$zipFile[$entryName] = $contents;
// or
-$zipFile->addFromString($entryName, $data);
+$zipFile->addFromString($entryName, $contents);
-// with compression method
-$zipFile->addFromString($entryName, $data, ZipFile::METHOD_DEFLATED); // Deflate compression
-$zipFile->addFromString($entryName, $data, ZipFile::METHOD_STORED); // No compression
-$zipFile->addFromString($entryName, $data, ZipFile::METHOD_BZIP2); // BZIP2 compression
+// you can specify a compression method
+$zipFile->addFromString($entryName, $contents, ZipFile::METHOD_STORED); // No compression
+$zipFile->addFromString($entryName, $contents, ZipFile::METHOD_DEFLATED); // Deflate compression
+$zipFile->addFromString($entryName, $contents, ZipFile::METHOD_BZIP2); // BZIP2 compression
```
-Add entry from stream.
+ **ZipFile::addFromStream** - adds a entry from the stream to the ZIP archive.
```php
-// $stream = fopen(...);
+// $stream = fopen(..., 'rb');
$zipFile->addFromStream($stream, $entryName);
+// or
+$zipFile[$entryName] = $stream;
-// with compression method
-$zipFile->addFromStream($stream, $entryName, ZipFile::METHOD_DEFLATED); // Deflate compression
+// you can specify a compression method
$zipFile->addFromStream($stream, $entryName, ZipFile::METHOD_STORED); // No compression
+$zipFile->addFromStream($stream, $entryName, ZipFile::METHOD_DEFLATED); // Deflate compression
$zipFile->addFromStream($stream, $entryName, ZipFile::METHOD_BZIP2); // BZIP2 compression
```
-Add empty dir
+ **ZipFile::addEmptyDir** - add a new directory.
```php
-// $dirName = "path/to/";
+// $path = "path/to/";
-$zipFile->addEmptyDir($dirName);
+$zipFile->addEmptyDir($path);
// or
-$zipFile[$dirName] = null;
+$zipFile[$path] = null;
```
-Add all entries form string contents.
+ **ZipFile::addAll** - adds all entries from an array.
```php
-$mapData = [
- 'file.txt' => 'file contents',
- 'path/to/file.txt' => 'another file contents',
- 'empty dir/' => null,
+$entries = [
+ 'file.txt' => 'file contents', // add an entry from the string contents
+ 'empty dir/' => null, // add empty directory
+ 'path/to/file.jpg' => fopen('..../filename', 'r'), // add an entry from the stream
+ 'path/to/file.dat' => new \SplFileInfo('..../filename'), // add an entry from the file
];
-$zipFile->addAll($mapData);
+$zipFile->addAll($entries);
```
-Add a directory **not recursively** to the archive.
+ **ZipFile::addDir** - adds files to the archive from the directory on the specified path without subdirectories.
```php
$zipFile->addDir($dirName);
-// with entry path
+// you can specify the path in the archive to which you want to put entries
$localPath = "to/path/";
$zipFile->addDir($dirName, $localPath);
-// with compression method for all files
-$zipFile->addDir($dirName, $localPath, ZipFile::METHOD_DEFLATED); // Deflate compression
+// you can specify a compression method
$zipFile->addDir($dirName, $localPath, ZipFile::METHOD_STORED); // No compression
+$zipFile->addDir($dirName, $localPath, ZipFile::METHOD_DEFLATED); // Deflate compression
$zipFile->addDir($dirName, $localPath, ZipFile::METHOD_BZIP2); // BZIP2 compression
```
-Add a directory **recursively** to the archive.
+ **ZipFile::addDirRecursive** - adds files to the archive from the directory on the specified path with subdirectories.
```php
$zipFile->addDirRecursive($dirName);
-// with entry path
+// you can specify the path in the archive to which you want to put entries
$localPath = "to/path/";
$zipFile->addDirRecursive($dirName, $localPath);
-// with compression method for all files
-$zipFile->addDirRecursive($dirName, $localPath, ZipFile::METHOD_DEFLATED); // Deflate compression
+// you can specify a compression method
$zipFile->addDirRecursive($dirName, $localPath, ZipFile::METHOD_STORED); // No compression
+$zipFile->addDirRecursive($dirName, $localPath, ZipFile::METHOD_DEFLATED); // Deflate compression
$zipFile->addDirRecursive($dirName, $localPath, ZipFile::METHOD_BZIP2); // BZIP2 compression
```
-Add a files from directory iterator.
+ **ZipFile::addFilesFromIterator** - adds files from the iterator of directories.
```php
-// $directoryIterator = new \DirectoryIterator($dir); // not recursive
-// $directoryIterator = new \RecursiveDirectoryIterator($dir); // recursive
+// $directoryIterator = new \DirectoryIterator($dir); // without subdirectories
+// $directoryIterator = new \RecursiveDirectoryIterator($dir); // with subdirectories
$zipFile->addFilesFromIterator($directoryIterator);
-// with entry path
+// you can specify the path in the archive to which you want to put entries
$localPath = "to/path/";
$zipFile->addFilesFromIterator($directoryIterator, $localPath);
// or
$zipFile[$localPath] = $directoryIterator;
-// with compression method for all files
-$zipFile->addFilesFromIterator($directoryIterator, $localPath, ZipFile::METHOD_DEFLATED); // Deflate compression
+// you can specify a compression method
$zipFile->addFilesFromIterator($directoryIterator, $localPath, ZipFile::METHOD_STORED); // No compression
+$zipFile->addFilesFromIterator($directoryIterator, $localPath, ZipFile::METHOD_DEFLATED); // Deflate compression
$zipFile->addFilesFromIterator($directoryIterator, $localPath, ZipFile::METHOD_BZIP2); // BZIP2 compression
```
-Example add a directory to the archive with ignoring files from directory iterator.
+Example with some files ignoring:
```php
$ignoreFiles = [
"file_ignore.txt",
"dir_ignore/sub dir ignore/"
];
-// use \DirectoryIterator for not recursive
-$directoryIterator = new \RecursiveDirectoryIterator($dir);
+// $directoryIterator = new \DirectoryIterator($dir); // without subdirectories
+// $directoryIterator = new \RecursiveDirectoryIterator($dir); // with subdirectories
-// use IgnoreFilesFilterIterator for not recursive
-$ignoreIterator = new IgnoreFilesRecursiveFilterIterator(
+// use \PhpZip\Util\Iterator\IgnoreFilesFilterIterator for non-recursive search
+$ignoreIterator = new \PhpZip\Util\Iterator\IgnoreFilesRecursiveFilterIterator(
$directoryIterator,
$ignoreFiles
);
$zipFile->addFilesFromIterator($ignoreIterator);
```
-Add a files **recursively** from [glob pattern](https://en.wikipedia.org/wiki/Glob_(programming)) to the archive.
-```php
-$globPattern = '**.{jpg,jpeg,png,gif}'; // example glob pattern -> add all .jpg, .jpeg, .png and .gif files
-
-$zipFile->addFilesFromGlobRecursive($dir, $globPattern);
-
-// with entry path
-$localPath = "to/path/";
-$zipFile->addFilesFromGlobRecursive($dir, $globPattern, $localPath);
-
-// with compression method for all files
-$zipFile->addFilesFromGlobRecursive($dir, $globPattern, $localPath), ZipFile::METHOD_DEFLATED); // Deflate compression
-$zipFile->addFilesFromGlobRecursive($dir, $globPattern, $localPath), ZipFile::METHOD_STORED); // No compression
-$zipFile->addFilesFromGlobRecursive($dir, $globPattern, $localPath), ZipFile::METHOD_BZIP2); // BZIP2 compression
-```
-Add a files **not recursively** from [glob pattern](https://en.wikipedia.org/wiki/Glob_(programming)) to the archive.
+ **ZipFile::addFilesFromGlob** - adds files from a directory by [glob pattern](https://en.wikipedia.org/wiki/Glob_(programming)) without subdirectories.
```php
$globPattern = '**.{jpg,jpeg,png,gif}'; // example glob pattern -> add all .jpg, .jpeg, .png and .gif files
$zipFile->addFilesFromGlob($dir, $globPattern);
-// with entry path
+// you can specify the path in the archive to which you want to put entries
$localPath = "to/path/";
$zipFile->addFilesFromGlob($dir, $globPattern, $localPath);
-// with compression method for all files
-$zipFile->addFilesFromGlob($dir, $globPattern, $localPath), ZipFile::METHOD_DEFLATED); // Deflate compression
-$zipFile->addFilesFromGlob($dir, $globPattern, $localPath), ZipFile::METHOD_STORED); // No compression
-$zipFile->addFilesFromGlob($dir, $globPattern, $localPath), ZipFile::METHOD_BZIP2); // BZIP2 compression
+// you can specify a compression method
+$zipFile->addFilesFromGlob($dir, $globPattern, $localPath, ZipFile::METHOD_STORED); // No compression
+$zipFile->addFilesFromGlob($dir, $globPattern, $localPath, ZipFile::METHOD_DEFLATED); // Deflate compression
+$zipFile->addFilesFromGlob($dir, $globPattern, $localPath, ZipFile::METHOD_BZIP2); // BZIP2 compression
```
-Add a files **recursively** from [RegEx (Regular Expression) pattern](https://en.wikipedia.org/wiki/Regular_expression) to the archive.
+ **ZipFile::addFilesFromGlobRecursive** - adds files from a directory by [glob pattern](https://en.wikipedia.org/wiki/Glob_(programming)) with subdirectories.
```php
-$regexPattern = '/\.(jpe?g|png|gif)$/si'; // example regex pattern -> add all .jpg, .jpeg, .png and .gif files
+$globPattern = '**.{jpg,jpeg,png,gif}'; // example glob pattern -> add all .jpg, .jpeg, .png and .gif files
-$zipFile->addFilesFromRegexRecursive($dir, $regexPattern);
+$zipFile->addFilesFromGlobRecursive($dir, $globPattern);
-// with entry path
+// you can specify the path in the archive to which you want to put entries
$localPath = "to/path/";
-$zipFile->addFilesFromRegexRecursive($dir, $regexPattern, $localPath);
+$zipFile->addFilesFromGlobRecursive($dir, $globPattern, $localPath);
-// with compression method for all files
-$zipFile->addFilesFromRegexRecursive($dir, $regexPattern, $localPath, ZipFile::METHOD_DEFLATED); // Deflate compression
-$zipFile->addFilesFromRegexRecursive($dir, $regexPattern, $localPath, ZipFile::METHOD_STORED); // No compression
-$zipFile->addFilesFromRegexRecursive($dir, $regexPattern, $localPath, ZipFile::METHOD_BZIP2); // BZIP2 compression
+// you can specify a compression method
+$zipFile->addFilesFromGlobRecursive($dir, $globPattern, $localPath, ZipFile::METHOD_STORED); // No compression
+$zipFile->addFilesFromGlobRecursive($dir, $globPattern, $localPath, ZipFile::METHOD_DEFLATED); // Deflate compression
+$zipFile->addFilesFromGlobRecursive($dir, $globPattern, $localPath, ZipFile::METHOD_BZIP2); // BZIP2 compression
```
-Add a files **not recursively** from [RegEx (Regular Expression) pattern](https://en.wikipedia.org/wiki/Regular_expression) to the archive.
+ **ZipFile::addFilesFromRegex** - adds files from a directory by [PCRE pattern](https://en.wikipedia.org/wiki/Regular_expression) without subdirectories.
```php
$regexPattern = '/\.(jpe?g|png|gif)$/si'; // example regex pattern -> add all .jpg, .jpeg, .png and .gif files
$zipFile->addFilesFromRegex($dir, $regexPattern);
-// with entry path
+// you can specify the path in the archive to which you want to put entries
$localPath = "to/path/";
$zipFile->addFilesFromRegex($dir, $regexPattern, $localPath);
-// with compression method for all files
-$zipFile->addFilesFromRegex($dir, $regexPattern, $localPath, ZipFile::METHOD_DEFLATED); // Deflate compression
+// you can specify a compression method
$zipFile->addFilesFromRegex($dir, $regexPattern, $localPath, ZipFile::METHOD_STORED); // No compression
+$zipFile->addFilesFromRegex($dir, $regexPattern, $localPath, ZipFile::METHOD_DEFLATED); // Deflate compression
$zipFile->addFilesFromRegex($dir, $regexPattern, $localPath, ZipFile::METHOD_BZIP2); // BZIP2 compression
```
-Rename entry name.
+ **ZipFile::addFilesFromRegexRecursive** - adds files from a directory by [PCRE pattern](https://en.wikipedia.org/wiki/Regular_expression) with subdirectories.
```php
-$zipFile->rename($oldName, $newName);
+$regexPattern = '/\.(jpe?g|png|gif)$/si'; // example regex pattern -> add all .jpg, .jpeg, .png and .gif files
+
+$zipFile->addFilesFromRegexRecursive($dir, $regexPattern);
+
+// you can specify the path in the archive to which you want to put entries
+$localPath = "to/path/";
+$zipFile->addFilesFromRegexRecursive($dir, $regexPattern, $localPath);
+
+// you can specify a compression method
+$zipFile->addFilesFromRegexRecursive($dir, $regexPattern, $localPath, ZipFile::METHOD_STORED); // No compression
+$zipFile->addFilesFromRegexRecursive($dir, $regexPattern, $localPath, ZipFile::METHOD_DEFLATED); // Deflate compression
+$zipFile->addFilesFromRegexRecursive($dir, $regexPattern, $localPath, ZipFile::METHOD_BZIP2); // BZIP2 compression
```
-Delete entry by name.
+#### Deleting entries from the archive
+ **ZipFile::deleteFromName** - deletes an entry in the archive using its name.
```php
$zipFile->deleteFromName($entryName);
```
-Delete entries from [glob pattern](https://en.wikipedia.org/wiki/Glob_(programming)).
+ **ZipFile::deleteFromGlob** - deletes a entries in the archive using [glob pattern](https://en.wikipedia.org/wiki/Glob_(programming)).
```php
$globPattern = '**.{jpg,jpeg,png,gif}'; // example glob pattern -> delete all .jpg, .jpeg, .png and .gif files
$zipFile->deleteFromGlob($globPattern);
```
-Delete entries from [RegEx (Regular Expression) pattern](https://en.wikipedia.org/wiki/Regular_expression).
+ **ZipFile::deleteFromRegex** - deletes a entries in the archive using [PCRE pattern](https://en.wikipedia.org/wiki/Regular_expression).
```php
$regexPattern = '/\.(jpe?g|png|gif)$/si'; // example regex pattern -> delete all .jpg, .jpeg, .png and .gif files
$zipFile->deleteFromRegex($regexPattern);
```
-Delete all entries.
+ **ZipFile::deleteAll** - deletes all entries in the ZIP archive.
```php
$zipFile->deleteAll();
```
-Sets the compression level for entries.
+#### Working with entries and archive
+ **ZipFile::rename** - renames an entry defined by its name.
```php
-// This property is only used if the effective compression method is DEFLATED or BZIP2.
-// Legal values are ZipFile::LEVEL_DEFAULT_COMPRESSION or range from
-// ZipFile::LEVEL_BEST_SPEED to ZipFile::LEVEL_BEST_COMPRESSION.
-
-$compressionMethod = ZipFile::LEVEL_BEST_COMPRESSION;
-
-$zipFile->setCompressionLevel($compressionLevel);
+$zipFile->rename($oldName, $newName);
```
-Set comment archive.
+ **ZipFile::setCompressionLevel** - set the compression level for all files in the archive.
+
+> _Note that this method does not apply to entries that were added after this method was run._
+
+By default, the compression level is -1 (`\PhpZip\ZipFile::LEVEL_DEFAULT_COMPRESSION`) or the compression level specified in the archive for Deflate compression.
+
+The values -1 (`\PhpZip\ZipFile::LEVEL_DEFAULT_COMPRESSION`) and the range from 1 (`\PhpZip\ZipFile::LEVEL_BEST_SPEED`) to 9 (`\PhpZip\ZipFile::LEVEL_BEST_COMPRESSION`) are supported. The higher the number, the better and longer the compression.
+```php
+$zipFile->setCompressionLevel(\PhpZip\ZipFile::LEVEL_BEST_COMPRESSION);
+```
+ **ZipFile::setCompressionLevelEntry** - sets the compression level for the entry by its name.
+
+The values -1 (`\PhpZip\ZipFile::LEVEL_DEFAULT_COMPRESSION`) and the range from 1 (`\PhpZip\ZipFile::LEVEL_BEST_SPEED`) to 9 (`\PhpZip\ZipFile::LEVEL_BEST_COMPRESSION`) are supported. The higher the number, the better and longer the compression.
+```php
+$zipFile->setCompressionLevelEntry($entryName, \PhpZip\ZipFile::LEVEL_BEST_COMPRESSION);
+```
+ **ZipFile::setCompressionMethodEntry** - sets the compression method for the entry by its name.
+
+The following compression methods are available:
+- `\PhpZip\ZipFile::METHOD_STORED` - No compression
+- `\PhpZip\ZipFile::METHOD_DEFLATED` - Deflate compression
+- `\PhpZip\ZipFile::METHOD_BZIP2` - Bzip2 compression with the extension `ext-bz2`
+```php
+$zipFile->setCompressionMethodEntry($entryName, ZipFile::METHOD_DEFLATED);
+```
+ **ZipFile::setArchiveComment** - set the comment of a ZIP archive.
```php
$zipFile->setArchiveComment($commentArchive);
```
-Set comment zip entry.
+ **ZipFile::setEntryComment** - set the comment of an entry defined by its name.
```php
-$zipFile->setEntryComment($entryName, $entryComment);
+$zipFile->setEntryComment($entryName, $comment);
```
-Set a new password.
+ **ZipFile::matcher** - selecting entries in the archive to perform operations on them.
```php
-$zipFile->withNewPassword($password);
+$matcher = $zipFile->matcher();
```
-Set a new password and encryption method.
+Selecting files from the archive one at a time:
```php
-$encryptionMethod = ZipFile::ENCRYPTION_METHOD_WINZIP_AES; // default value
-$zipFile->withNewPassword($password, $encryptionMethod);
+$matcher
+ ->add('entry name')
+ ->add('another entry');
+```
+Select multiple files in the archive:
+```php
+$matcher->add([
+ 'entry name',
+ 'another entry name',
+ 'path/'
+]);
+```
+Selecting files by regular expression:
+```php
+$matcher->match('~\.jpe?g$~i');
+```
+Select all files in the archive:
+```php
+$matcher->all();
+```
+count() - gets the number of selected entries:
+```php
+$count = count($matcher);
+// or
+$count = $matcher->count();
+```
+getMatches() - returns a list of selected entries:
+```php
+$entries = $matcher->getMatches();
+// example array contents: ['entry name', 'another entry name'];
+```
+invoke() - invoke a callable function on selected entries:
+```php
+// example
+$matcher->invoke(function($entryName) use($zipFile) {
+ $newName = preg_replace('~\.(jpe?g)$~i', '.no_optimize.$1', $entryName);
+ $zipFile->rename($entryName, $newName);
+});
+```
+Functions for working on the selected entries:
+```php
+$matcher->delete(); // remove selected entries from a ZIP archive
+$matcher->setPassword($password); // sets a new password for the selected entries
+$matcher->setPassword($password, $encryptionMethod); // sets a new password and encryption method to selected entries
+$matcher->setEncryptionMethod($encryptionMethod); // sets the encryption method to the selected entries
+$matcher->disableEncryption(); // disables encryption for selected entries
+```
+#### Working with passwords
-// Support encryption methods:
-// ZipFile::ENCRYPTION_METHOD_TRADITIONAL - Traditional PKWARE Encryption
-// ZipFile::ENCRYPTION_METHOD_WINZIP_AES - WinZip AES Encryption
-```
-Remove password from all entries.
+Implemented support for encryption methods:
+- `\PhpZip\ZipFile::ENCRYPTION_METHOD_TRADITIONAL` - Traditional PKWARE encryption
+- `\PhpZip\ZipFile::ENCRYPTION_METHOD_WINZIP_AES_256` - WinZip AES encryption 256 bit (recommended)
+- `\PhpZip\ZipFile::ENCRYPTION_METHOD_WINZIP_AES_192` - WinZip AES encryption 192 bit
+- `\PhpZip\ZipFile::ENCRYPTION_METHOD_WINZIP_AES_128` - WinZip AES encryption 128 bit
+
+ **ZipFile::setReadPassword** - set the password for the open archive.
+
+> _Setting a password is not required for adding new entries or deleting existing ones, but if you want to extract the content or change the method / compression level, the encryption method, or change the password, in this case the password must be specified._
```php
-$zipFile->withoutPassword();
+$zipFile->setReadPassword($password);
```
-#### ZipAlign Usage
-Set archive alignment ([`zipalign`](https://developer.android.com/studio/command-line/zipalign.html)).
+ **ZipFile::setReadPasswordEntry** - gets a password for reading of an entry defined by its name.
```php
-// before save or output
-$zipFile->setAlign(4); // alternative command: zipalign -f -v 4 filename.zip
+$zipFile->setReadPasswordEntry($entryName, $password);
```
-#### Save Zip File or Output
-Save archive to a file.
+ **ZipFile::setPassword** - sets a new password for all files in the archive.
+
+> _Note that this method does not apply to entries that were added after this method was run._
+```php
+$zipFile->setPassword($password);
+```
+You can set the encryption method:
+```php
+$encryptionMethod = ZipFile::ENCRYPTION_METHOD_WINZIP_AES_256;
+$zipFile->setPassword($password, $encryptionMethod);
+```
+ **ZipFile::setPasswordEntry** - sets a new password of an entry defined by its name.
+```php
+$zipFile->setPasswordEntry($entryName, $password);
+```
+You can set the encryption method:
+```php
+$encryptionMethod = ZipFile::ENCRYPTION_METHOD_WINZIP_AES_256;
+$zipFile->setPasswordEntry($entryName, $password, $encryptionMethod);
+```
+ **ZipFile::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
+### Upgrade
+#### Upgrade version 2 to version 3.0
+Update the major version in the file `composer.json` to `^3.0`.
```json
{
"require": {
@@ -517,11 +788,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 +807,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 @@
-
-
+ * An offset to check for. + *
+ * @return boolean true on success or false on failure. + * + *+ * The return value will be casted to boolean if non-boolean was returned. + * @since 5.0.0 + */ + public function offsetExists($offset) + { + return isset($this->collection[$offset]); + } + + /** + * Offset to retrieve + * @link http://php.net/manual/en/arrayaccess.offsetget.php + * @param mixed $offset
+ * The offset to retrieve. + *
+ * @return mixed Can return all value types. + * @since 5.0.0 + */ + public function offsetGet($offset) + { + return $this->get($offset); + } + + /** + * Offset to set + * @link http://php.net/manual/en/arrayaccess.offsetset.php + * @param mixed $offset+ * The offset to assign the value to. + *
+ * @param mixed $value+ * The value to set. + *
+ * @return void + * @throws InvalidArgumentException + * @since 5.0.0 + */ + public function offsetSet($offset, $value) + { + if ($value instanceof ExtraField) { + assert($offset == $value::getHeaderId()); + $this->add($value); + } else { + throw new InvalidArgumentException('value is not instanceof ' . ExtraField::class); + } + } + + /** + * Offset to unset + * @link http://php.net/manual/en/arrayaccess.offsetunset.php + * @param mixed $offset+ * The offset to unset. + *
+ * @return void + * @since 5.0.0 + */ + public function offsetUnset($offset) + { + $this->remove($offset); + } + + /** + * Return the current element + * @link http://php.net/manual/en/iterator.current.php + * @return mixed Can return any type. + * @since 5.0.0 + */ + public function current() + { + return current($this->collection); + } + + /** + * Move forward to next element + * @link http://php.net/manual/en/iterator.next.php + * @return void Any returned value is ignored. + * @since 5.0.0 + */ + public function next() + { + next($this->collection); + } + + /** + * Return the key of the current element + * @link http://php.net/manual/en/iterator.key.php + * @return mixed scalar on success, or null on failure. + * @since 5.0.0 + */ + public function key() + { + return key($this->collection); + } + + /** + * Checks if current position is valid + * @link http://php.net/manual/en/iterator.valid.php + * @return boolean The return value will be casted to boolean and then evaluated. + * Returns true on success or false on failure. + * @since 5.0.0 + */ + public function valid() + { + return $this->offsetExists($this->key()); + } + + /** + * Rewind the Iterator to the first element + * @link http://php.net/manual/en/iterator.rewind.php + * @return void Any returned value is ignored. + * @since 5.0.0 + */ + public function rewind() + { + reset($this->collection); + } + + /** + * If clone extra fields. + */ + public function __clone() + { + foreach ($this->collection as $k => $v) { + $this->collection[$k] = clone $v; + } + } +} diff --git a/src/PhpZip/Extra/ExtraFieldsFactory.php b/src/PhpZip/Extra/ExtraFieldsFactory.php new file mode 100644 index 0000000..2e9fd82 --- /dev/null +++ b/src/PhpZip/Extra/ExtraFieldsFactory.php @@ -0,0 +1,100 @@ + $headerId || $headerId > 0xffff) { + throw new ZipException('headerId out of range'); + } + + /** + * @var ExtraField $extraField + */ + if (isset(self::getRegistry()[$headerId])) { + $extraClassName = self::getRegistry()[$headerId]; + $extraField = new $extraClassName; + if ($extraField::getHeaderId() !== $headerId) { + throw new ZipException('Runtime error support headerId ' . $headerId); + } + } else { + $extraField = new DefaultExtraField($headerId); + } + return $extraField; + } + + /** + * Registered extra field classes. + * + * @return array + */ + protected static function getRegistry() + { + if (null === self::$registry) { + self::$registry[WinZipAesEntryExtraField::getHeaderId()] = WinZipAesEntryExtraField::class; + self::$registry[NtfsExtraField::getHeaderId()] = NtfsExtraField::class; + self::$registry[Zip64ExtraField::getHeaderId()] = Zip64ExtraField::class; + } + return self::$registry; + } + + /** + * @return WinZipAesEntryExtraField + */ + public static function createWinZipAesEntryExtra() + { + return new WinZipAesEntryExtraField(); + } + + /** + * @return NtfsExtraField + */ + public static function createNtfsExtra() + { + return new NtfsExtraField(); + } + + /** + * @param ZipEntry $entry + * @return Zip64ExtraField + */ + public static function createZip64Extra(ZipEntry $entry) + { + return new Zip64ExtraField($entry); + } +} diff --git a/src/PhpZip/Extra/Fields/DefaultExtraField.php b/src/PhpZip/Extra/Fields/DefaultExtraField.php new file mode 100644 index 0000000..77af380 --- /dev/null +++ b/src/PhpZip/Extra/Fields/DefaultExtraField.php @@ -0,0 +1,71 @@ + $headerId || $headerId > 0xffff) { + throw new ZipException('headerId out of range'); + } + self::$headerId = $headerId; + } + + /** + * Returns the Header ID (type) of this Extra Field. + * The Header ID is an unsigned short integer (two bytes) + * which must be constant during the life cycle of this object. + * + * @return int + */ + public static function getHeaderId() + { + return self::$headerId & 0xffff; + } + + /** + * Serializes a Data Block. + * @return string + */ + public function serialize() + { + return $this->data; + } + + /** + * Initializes this Extra Field by deserializing a Data Block. + * @param string $data + */ + public function deserialize($data) + { + $this->data = $data; + } +} diff --git a/src/PhpZip/Extra/Fields/NtfsExtraField.php b/src/PhpZip/Extra/Fields/NtfsExtraField.php new file mode 100644 index 0000000..45efb36 --- /dev/null +++ b/src/PhpZip/Extra/Fields/NtfsExtraField.php @@ -0,0 +1,133 @@ +mtime = PackUtil::unpackLongLE(substr($tagData, 0, 8)) / 10000000 - 11644473600; + $this->atime = PackUtil::unpackLongLE(substr($tagData, 8, 8)) / 10000000 - 11644473600; + $this->ctime = PackUtil::unpackLongLE(substr($tagData, 16, 8)) / 10000000 - 11644473600; + } + } + + /** + * Serializes a Data Block. + * @return string + */ + public function serialize() + { + $serialize = ''; + if (null !== $this->mtime && null !== $this->atime && null !== $this->ctime) { + $mtimeLong = ($this->mtime + 11644473600) * 10000000; + $atimeLong = ($this->atime + 11644473600) * 10000000; + $ctimeLong = ($this->ctime + 11644473600) * 10000000; + + $serialize .= pack('Vvv', 0, 1, 8 * 3) + . PackUtil::packLongLE($mtimeLong) + . PackUtil::packLongLE($atimeLong) + . PackUtil::packLongLE($ctimeLong); + } + return $serialize; + } + + /** + * @return int + */ + public function getMtime() + { + return $this->mtime; + } + + /** + * @param int $mtime + */ + public function setMtime($mtime) + { + $this->mtime = (int)$mtime; + } + + /** + * @return int + */ + public function getAtime() + { + return $this->atime; + } + + /** + * @param int $atime + */ + public function setAtime($atime) + { + $this->atime = (int)$atime; + } + + /** + * @return int + */ + public function getCtime() + { + return $this->ctime; + } + + /** + * @param int $ctime + */ + public function setCtime($ctime) + { + $this->ctime = (int)$ctime; + } +} diff --git a/src/PhpZip/Extra/WinZipAesEntryExtraField.php b/src/PhpZip/Extra/Fields/WinZipAesEntryExtraField.php similarity index 65% rename from src/PhpZip/Extra/WinZipAesEntryExtraField.php rename to src/PhpZip/Extra/Fields/WinZipAesEntryExtraField.php index 6f32ce2..83b5e97 100644 --- a/src/PhpZip/Extra/WinZipAesEntryExtraField.php +++ b/src/PhpZip/Extra/Fields/WinZipAesEntryExtraField.php @@ -1,7 +1,10 @@ 0x01, self::KEY_STRENGTH_192BIT => 0x02, self::KEY_STRENGTH_256BIT => 0x03 ]; + protected static $encryptionMethods = [ + self::KEY_STRENGTH_128BIT => ZipFileInterface::ENCRYPTION_METHOD_WINZIP_AES_128, + self::KEY_STRENGTH_192BIT => ZipFileInterface::ENCRYPTION_METHOD_WINZIP_AES_192, + self::KEY_STRENGTH_256BIT => ZipFileInterface::ENCRYPTION_METHOD_WINZIP_AES_256 + ]; + /** * Vendor version. * * @var int */ - private $vendorVersion = self::VV_AE_1; + protected $vendorVersion = self::VV_AE_1; /** * Encryption strength. * * @var int */ - private $encryptionStrength = self::KEY_STRENGTH_256BIT; + protected $encryptionStrength = self::KEY_STRENGTH_256BIT; /** * Zip compression method. * * @var int */ - private $method; + protected $method; /** * Returns the Header ID (type) of this Extra Field. @@ -71,21 +80,6 @@ class WinZipAesEntryExtraField extends ExtraField return 0x9901; } - /** - * Returns the Data Size of this Extra Field. - * The Data Size is an unsigned short integer (two bytes) - * which indicates the length of the Data Block in bytes and does not - * include its own size in this Extra Field. - * This property may be initialized by calling ExtraField::readFrom. - * - * @return int The size of the Data Block in bytes - * or 0 if unknown. - */ - public function getDataSize() - { - return self::DATA_SIZE; - } - /** * Returns the vendor version. * @@ -155,6 +149,32 @@ class WinZipAesEntryExtraField extends ExtraField return $this->method & 0xffff; } + /** + * Internal encryption method. + * + * @return int + */ + public function getEncryptionMethod() + { + return isset(self::$encryptionMethods[$this->getKeyStrength()]) ? + self::$encryptionMethods[$this->getKeyStrength()] : + self::$encryptionMethods[self::KEY_STRENGTH_256BIT]; + } + + /** + * @param int $encryptionMethod + * @return int + * @throws ZipException + */ + public static function getKeyStrangeFromEncryptionMethod($encryptionMethod) + { + $flipKey = array_flip(self::$encryptionMethods); + if (!isset($flipKey[$encryptionMethod])) { + throw new ZipException("Unsupport encryption method " . $encryptionMethod); + } + return $flipKey[$encryptionMethod]; + } + /** * Sets compression method. * @@ -169,37 +189,6 @@ class WinZipAesEntryExtraField extends ExtraField $this->method = $compressionMethod; } - /** - * Initializes this Extra Field by deserializing a Data Block of - * size bytes $size from the resource $handle at the zero based offset $off. - * - * @param resource $handle - * @param int $off Offset bytes - * @param int $size Size - * @throws ZipException - */ - public function readFrom($handle, $off, $size) - { - if (self::DATA_SIZE != $size) - throw new ZipException(); - - fseek($handle, $off, SEEK_SET); - /** - * @var int $vendorVersion - * @var int $vendorId - * @var int $keyStrength - * @var int $method - */ - $unpack = unpack('vvendorVersion/vvendorId/ckeyStrength/vmethod', fread($handle, 7)); - extract($unpack); - $this->setVendorVersion($vendorVersion); - if (self::VENDOR_ID != $vendorId) { - throw new ZipException(); - } - $this->setKeyStrength(self::keyStrength($keyStrength)); // checked - $this->setMethod($method); - } - /** * Set key strength. * @@ -218,19 +207,50 @@ class WinZipAesEntryExtraField extends ExtraField */ public static function encryptionStrength($keyStrength) { - return isset(self::$keyStrengths[$keyStrength]) ? self::$keyStrengths[$keyStrength] : self::$keyStrengths[self::KEY_STRENGTH_128BIT]; + return isset(self::$keyStrengths[$keyStrength]) ? + self::$keyStrengths[$keyStrength] : + self::$keyStrengths[self::KEY_STRENGTH_128BIT]; } /** - * Serializes a Data Block of ExtraField::getDataSize bytes to the - * resource $handle at the zero based offset $off. - * - * @param resource $handle - * @param int $off Offset bytes + * Serializes a Data Block. + * @return string */ - public function writeTo($handle, $off) + public function serialize() { - fseek($handle, $off, SEEK_SET); - fwrite($handle, pack('vvcv', $this->vendorVersion, self::VENDOR_ID, $this->encryptionStrength, $this->method)); + return pack( + 'vvcv', + $this->vendorVersion, + self::VENDOR_ID, + $this->encryptionStrength, + $this->method + ); } -} \ No newline at end of file + + /** + * Initializes this Extra Field by deserializing a Data Block. + * @param string $data + * @throws ZipException + */ + public function deserialize($data) + { + $size = strlen($data); + if (self::DATA_SIZE !== $size) { + throw new ZipException('WinZip AES Extra data invalid size: ' . $size . '. Must be ' . self::DATA_SIZE); + } + + /** + * @var int $vendorVersion + * @var int $vendorId + * @var int $keyStrength + * @var int $method + */ + $unpack = unpack('vvendorVersion/vvendorId/ckeyStrength/vmethod', $data); + $this->setVendorVersion($unpack['vendorVersion']); + if (self::VENDOR_ID !== $unpack['vendorId']) { + throw new ZipException('Vendor id invalid: ' . $unpack['vendorId'] . '. Must be ' . self::VENDOR_ID); + } + $this->setKeyStrength(self::keyStrength($unpack['keyStrength'])); // checked + $this->setMethod($unpack['method']); + } +} diff --git a/src/PhpZip/Extra/Fields/Zip64ExtraField.php b/src/PhpZip/Extra/Fields/Zip64ExtraField.php new file mode 100644 index 0000000..52c3e38 --- /dev/null +++ b/src/PhpZip/Extra/Fields/Zip64ExtraField.php @@ -0,0 +1,118 @@ +setEntry($entry); + } + } + + /** + * @param ZipEntry $entry + */ + public function setEntry(ZipEntry $entry) + { + $this->entry = $entry; + } + + /** + * Returns the Header ID (type) of this Extra Field. + * The Header ID is an unsigned short integer (two bytes) + * which must be constant during the life cycle of this object. + * + * @return int + */ + public static function getHeaderId() + { + return 0x0001; + } + + /** + * Serializes a Data Block. + * @return string + * @throws RuntimeException + */ + public function serialize() + { + if (null === $this->entry) { + throw new RuntimeException("entry is null"); + } + $data = ''; + // Write out Uncompressed Size. + $size = $this->entry->getSize(); + if (0xffffffff <= $size) { + $data .= PackUtil::packLongLE($size); + } + // Write out Compressed Size. + $compressedSize = $this->entry->getCompressedSize(); + if (0xffffffff <= $compressedSize) { + $data .= PackUtil::packLongLE($compressedSize); + } + // Write out Relative Header Offset. + $offset = $this->entry->getOffset(); + if (0xffffffff <= $offset) { + $data .= PackUtil::packLongLE($offset); + } + return $data; + } + + /** + * Initializes this Extra Field by deserializing a Data Block. + * @param string $data + * @throws RuntimeException + */ + public function deserialize($data) + { + if (null === $this->entry) { + throw new RuntimeException("entry is null"); + } + $off = 0; + // Read in Uncompressed Size. + $size = $this->entry->getSize(); + if (0xffffffff <= $size) { + assert(0xffffffff === $size); + $this->entry->setSize(PackUtil::unpackLongLE(substr($data, $off, 8))); + $off += 8; + } + // Read in Compressed Size. + $compressedSize = $this->entry->getCompressedSize(); + if (0xffffffff <= $compressedSize) { + assert(0xffffffff === $compressedSize); + $this->entry->setCompressedSize(PackUtil::unpackLongLE(substr($data, $off, 8))); + $off += 8; + } + // Read in Relative Header Offset. + $offset = $this->entry->getOffset(); + if (0xffffffff <= $offset) { + assert(0xffffffff, $offset); + $this->entry->setOffset(PackUtil::unpackLongLE(substr($data, $off, 8))); + } + } +} diff --git a/src/PhpZip/Extra/NtfsExtraField.php b/src/PhpZip/Extra/NtfsExtraField.php deleted file mode 100644 index da09225..0000000 --- a/src/PhpZip/Extra/NtfsExtraField.php +++ /dev/null @@ -1,176 +0,0 @@ -rawData); - } - - /** - * Initializes this Extra Field by deserializing a Data Block of - * size bytes $size from the resource $handle at the zero based offset $off. - * - * @param resource $handle - * @param int $off Offset bytes - * @param int $size Size - * @throws ZipException If size out of range - */ - public function readFrom($handle, $off, $size) - { - if (0x0000 > $size || $size > 0xffff) { - throw new ZipException('size out of range'); - } - if ($size > 0) { - $off += 4; - fseek($handle, $off, SEEK_SET); - - $unpack = unpack('vtag/vsizeAttr', fread($handle, 4)); - if (24 === $unpack['sizeAttr']) { - $tagData = fread($handle, $unpack['sizeAttr']); - - $this->mtime = PackUtil::unpackLongLE(substr($tagData, 0, 8)) / 10000000 - 11644473600; - $this->atime = PackUtil::unpackLongLE(substr($tagData, 8, 8)) / 10000000 - 11644473600; - $this->ctime = PackUtil::unpackLongLE(substr($tagData, 16, 8)) / 10000000 - 11644473600; - } - $off += $unpack['sizeAttr']; - - if ($size > $off) { - $this->rawData .= fread($handle, $size - $off); - } - } - } - - /** - * Serializes a Data Block of ExtraField::getDataSize bytes to the - * resource $handle at the zero based offset $off. - * - * @param resource $handle - * @param int $off Offset bytes - */ - public function writeTo($handle, $off) - { - if (null !== $this->mtime && null !== $this->atime && null !== $this->ctime) { - fseek($handle, $off, SEEK_SET); - fwrite($handle, pack('Vvv', 0, 1, 8 * 3 + strlen($this->rawData))); - $mtimeLong = ($this->mtime + 11644473600) * 10000000; - fwrite($handle, PackUtil::packLongLE($mtimeLong)); - $atimeLong = ($this->atime + 11644473600) * 10000000; - fwrite($handle, PackUtil::packLongLE($atimeLong)); - $ctimeLong = ($this->ctime + 11644473600) * 10000000; - fwrite($handle, PackUtil::packLongLE($ctimeLong)); - if (!empty($this->rawData)) { - fwrite($handle, $this->rawData); - } - } - } - - /** - * @return int - */ - public function getMtime() - { - return $this->mtime; - } - - /** - * @param int $mtime - */ - public function setMtime($mtime) - { - $this->mtime = (int)$mtime; - } - - /** - * @return int - */ - public function getAtime() - { - return $this->atime; - } - - /** - * @param int $atime - */ - public function setAtime($atime) - { - $this->atime = (int)$atime; - } - - /** - * @return int - */ - public function getCtime() - { - return $this->ctime; - } - - /** - * @param int $ctime - */ - public function setCtime($ctime) - { - $this->ctime = (int)$ctime; - } - -} \ No newline at end of file diff --git a/src/PhpZip/Mapper/OffsetPositionMapper.php b/src/PhpZip/Mapper/OffsetPositionMapper.php index 038cc47..7ea9116 100644 --- a/src/PhpZip/Mapper/OffsetPositionMapper.php +++ b/src/PhpZip/Mapper/OffsetPositionMapper.php @@ -1,4 +1,5 @@ offset = $offset; + $this->offset = (int)$offset; } /** @@ -39,4 +40,4 @@ class OffsetPositionMapper extends PositionMapper { return parent::unmap($position) - $this->offset; } -} \ No newline at end of file +} diff --git a/src/PhpZip/Mapper/PositionMapper.php b/src/PhpZip/Mapper/PositionMapper.php index a7b02a4..e5d67c8 100644 --- a/src/PhpZip/Mapper/PositionMapper.php +++ b/src/PhpZip/Mapper/PositionMapper.php @@ -1,4 +1,5 @@ endOfCentralDirectory = new EndOfCentralDirectory(); - } - - /** - * Reads the central directory from the given seekable byte channel - * and populates the internal tables with ZipEntry instances. - * - * The ZipEntry's will know all data that can be obtained from the - * central directory alone, but not the data that requires the local - * file header or additional data to be read. - * - * @param resource $inputStream - * @throws ZipException - */ - public function mountCentralDirectory($inputStream) - { - $this->modifiedEntries = []; - $this->checkZipFileSignature($inputStream); - $this->endOfCentralDirectory->findCentralDirectory($inputStream); - - $numEntries = $this->endOfCentralDirectory->getCentralDirectoryEntriesSize(); - $entries = []; - for (; $numEntries > 0; $numEntries--) { - $entry = new ZipReadEntry($inputStream); - $entry->setCentralDirectory($this); - // Re-load virtual offset after ZIP64 Extended Information - // Extra Field may have been parsed, map it to the real - // offset and conditionally update the preamble size from it. - $lfhOff = $this->endOfCentralDirectory->getMapper()->map($entry->getOffset()); - if ($lfhOff < $this->endOfCentralDirectory->getPreamble()) { - $this->endOfCentralDirectory->setPreamble($lfhOff); - } - $entries[$entry->getName()] = $entry; - } - - if (0 !== $numEntries % 0x10000) { - throw new ZipException("Expected " . abs($numEntries) . - ($numEntries > 0 ? " more" : " less") . - " entries in the Central Directory!"); - } - $this->entries = $entries; - - if ($this->endOfCentralDirectory->getPreamble() + $this->endOfCentralDirectory->getPostamble() >= fstat($inputStream)['size']) { - assert(0 === $numEntries); - $this->checkZipFileSignature($inputStream); - } - } - - /** - * Check zip file signature - * - * @param resource $inputStream - * @throws ZipException if this not .ZIP file. - */ - private function checkZipFileSignature($inputStream) - { - rewind($inputStream); - // Constraint: A ZIP file must start with a Local File Header - // or a (ZIP64) End Of Central Directory Record if it's empty. - $signatureBytes = fread($inputStream, 4); - if (strlen($signatureBytes) < 4) { - throw new ZipException("Invalid zip file."); - } - $signature = unpack('V', $signatureBytes)[1]; - if ( - ZipEntry::LOCAL_FILE_HEADER_SIG !== $signature - && EndOfCentralDirectory::ZIP64_END_OF_CENTRAL_DIRECTORY_RECORD_SIG !== $signature - && EndOfCentralDirectory::END_OF_CENTRAL_DIRECTORY_RECORD_SIG !== $signature - ) { - throw new ZipException("Expected Local File Header or (ZIP64) End Of Central Directory Record! Signature: " . $signature); - } - } - - /** - * Set compression method for new or rewrites entries. - * @param int $compressionLevel - * @throws InvalidArgumentException - * @see ZipFile::LEVEL_DEFAULT_COMPRESSION - * @see ZipFile::LEVEL_BEST_SPEED - * @see ZipFile::LEVEL_BEST_COMPRESSION - */ - public function setCompressionLevel($compressionLevel = ZipFile::LEVEL_DEFAULT_COMPRESSION) - { - if ($compressionLevel < ZipFile::LEVEL_DEFAULT_COMPRESSION || - $compressionLevel > ZipFile::LEVEL_BEST_COMPRESSION - ) { - throw new InvalidArgumentException('Invalid compression level. Minimum level ' . - ZipFile::LEVEL_DEFAULT_COMPRESSION . '. Maximum level ' . ZipFile::LEVEL_BEST_COMPRESSION); - } - $this->compressionLevel = $compressionLevel; - } - - /** - * @return ZipEntry[] - */ - public function &getEntries() - { - return $this->entries; - } - - /** - * @param string $entryName - * @return ZipEntry - * @throws ZipNotFoundEntry - */ - public function getEntry($entryName) - { - if (!isset($this->entries[$entryName])) { - throw new ZipNotFoundEntry('Zip entry ' . $entryName . ' not found'); - } - return $this->entries[$entryName]; - } - - /** - * @param string $entryName - * @return ZipEntry - * @throws ZipNotFoundEntry - */ - public function getModifiedEntry($entryName){ - if (!isset($this->modifiedEntries[$entryName])) { - throw new ZipNotFoundEntry('Zip modified entry ' . $entryName . ' not found'); - } - return $this->modifiedEntries[$entryName]; - } - - /** - * @return EndOfCentralDirectory - */ - public function getEndOfCentralDirectory() - { - return $this->endOfCentralDirectory; - } - - public function getArchiveComment() - { - return null === $this->endOfCentralDirectory->getComment() ? - '' : - $this->endOfCentralDirectory->getComment(); - } - - /** - * Set entry comment - * @param string $entryName - * @param string|null $comment - * @throws ZipNotFoundEntry - */ - public function setEntryComment($entryName, $comment) - { - if (isset($this->modifiedEntries[$entryName])) { - $this->modifiedEntries[$entryName]->setComment($comment); - } elseif (isset($this->entries[$entryName])) { - $entry = clone $this->entries[$entryName]; - $entry->setComment($comment); - $this->putInModified($entryName, $entry); - } else { - throw new ZipNotFoundEntry("Not found entry " . $entryName); - } - } - - /** - * @param string|null $password - * @param int|null $encryptionMethod - */ - public function setNewPassword($password, $encryptionMethod = null) - { - $this->password = $password; - $this->encryptionMethod = $encryptionMethod; - $this->clearPassword = $password === null; - } - - /** - * @return int|null - */ - public function getZipAlign() - { - return $this->zipAlign; - } - - /** - * @param int|null $zipAlign - */ - public function setZipAlign($zipAlign = null) - { - if (null === $zipAlign) { - $this->zipAlign = null; - return; - } - $this->zipAlign = (int)$zipAlign; - } - - /** - * Put modification or new entries. - * - * @param $entryName - * @param ZipEntry $entry - */ - public function putInModified($entryName, ZipEntry $entry) - { - $this->modifiedEntries[$entryName] = $entry; - } - - /** - * @param string $entryName - * @throws ZipNotFoundEntry - */ - public function deleteEntry($entryName) - { - if (isset($this->entries[$entryName])) { - $this->modifiedEntries[$entryName] = null; - } elseif (isset($this->modifiedEntries[$entryName])) { - unset($this->modifiedEntries[$entryName]); - } else { - throw new ZipNotFoundEntry("Not found entry " . $entryName); - } - } - - /** - * @param string $regexPattern - * @return bool - */ - public function deleteEntriesFromRegex($regexPattern) - { - $count = 0; - foreach ($this->modifiedEntries as $entryName => &$entry) { - if (preg_match($regexPattern, $entryName)) { - unset($entry); - $count++; - } - } - foreach ($this->entries as $entryName => $entry) { - if (preg_match($regexPattern, $entryName)) { - $this->modifiedEntries[$entryName] = null; - $count++; - } - } - return $count > 0; - } - - /** - * @param string $oldName - * @param string $newName - * @throws InvalidArgumentException - * @throws ZipNotFoundEntry - */ - public function rename($oldName, $newName) - { - $oldName = (string)$oldName; - $newName = (string)$newName; - - if (isset($this->entries[$newName]) || isset($this->modifiedEntries[$newName])) { - throw new InvalidArgumentException("New entry name " . $newName . ' is exists.'); - } - - if (isset($this->modifiedEntries[$oldName]) || isset($this->entries[$oldName])) { - $newEntry = clone (isset($this->modifiedEntries[$oldName]) ? - $this->modifiedEntries[$oldName] : - $this->entries[$oldName]); - $newEntry->setName($newName); - - $this->modifiedEntries[$oldName] = null; - $this->modifiedEntries[$newName] = $newEntry; - return; - } - throw new ZipNotFoundEntry("Not found entry " . $oldName); - } - - /** - * Delete all entries. - */ - public function deleteAll() - { - $this->modifiedEntries = []; - foreach ($this->entries as $entry) { - $this->modifiedEntries[$entry->getName()] = null; - } - } - - /** - * @param resource $outputStream - */ - public function writeArchive($outputStream) - { - /** - * @var ZipEntry[] $memoryEntriesResult - */ - $memoryEntriesResult = []; - foreach ($this->entries as $entryName => $entry) { - if (isset($this->modifiedEntries[$entryName])) continue; - - if ( - (null !== $this->password || $this->clearPassword) && - $entry->isEncrypted() && - $entry->getPassword() !== null && - ( - $entry->getPassword() !== $this->password || - $entry->getEncryptionMethod() !== $this->encryptionMethod - ) - ) { - $prototypeEntry = new ZipNewStringEntry($entry->getEntryContent()); - $prototypeEntry->setName($entry->getName()); - $prototypeEntry->setMethod($entry->getMethod()); - $prototypeEntry->setTime($entry->getTime()); - $prototypeEntry->setExternalAttributes($entry->getExternalAttributes()); - $prototypeEntry->setExtra($entry->getExtra()); - $prototypeEntry->setPassword($this->password, $this->encryptionMethod); - if ($this->clearPassword) { - $prototypeEntry->clearEncryption(); - } - } else { - $prototypeEntry = clone $entry; - } - $memoryEntriesResult[$entryName] = $prototypeEntry; - } - - foreach ($this->modifiedEntries as $entryName => $outputEntry) { - if (null === $outputEntry) { // remove marked entry - unset($memoryEntriesResult[$entryName]); - } else { - if (null !== $this->password) { - $outputEntry->setPassword($this->password, $this->encryptionMethod); - } - $memoryEntriesResult[$entryName] = $outputEntry; - } - } - - foreach ($memoryEntriesResult as $key => $outputEntry) { - $outputEntry->setCentralDirectory($this); - $outputEntry->writeEntry($outputStream); - } - $centralDirectoryOffset = ftell($outputStream); - foreach ($memoryEntriesResult as $key => $outputEntry) { - if (!$this->writeCentralFileHeader($outputStream, $outputEntry)) { - unset($memoryEntriesResult[$key]); - } - } - $centralDirectoryEntries = sizeof($memoryEntriesResult); - $this->getEndOfCentralDirectory()->writeEndOfCentralDirectory( - $outputStream, - $centralDirectoryEntries, - $centralDirectoryOffset - ); - } - - /** - * Writes a Central File Header record. - * - * @param resource $outputStream - * @param ZipEntry $entry - * @return bool false if and only if the record has been skipped, - * i.e. not written for some other reason than an I/O error. - */ - private function writeCentralFileHeader($outputStream, ZipEntry $entry) - { - $compressedSize = $entry->getCompressedSize(); - $size = $entry->getSize(); - // This test MUST NOT include the CRC-32 because VV_AE_2 sets it to - // UNKNOWN! - if (ZipEntry::UNKNOWN === ($compressedSize | $size)) { - return false; - } - $extra = $entry->getExtra(); - $extraSize = strlen($extra); - - $commentLength = strlen($entry->getComment()); - fwrite( - $outputStream, - pack( - 'VvvvvVVVVvvvvvVV', - // central file header signature 4 bytes (0x02014b50) - self::CENTRAL_FILE_HEADER_SIG, - // version made by 2 bytes - ($entry->getPlatform() << 8) | 63, - // version needed to extract 2 bytes - $entry->getVersionNeededToExtract(), - // general purpose bit flag 2 bytes - $entry->getGeneralPurposeBitFlags(), - // compression method 2 bytes - $entry->getMethod(), - // last mod file datetime 4 bytes - $entry->getDosTime(), - // crc-32 4 bytes - $entry->getCrc(), - // compressed size 4 bytes - $entry->getCompressedSize(), - // uncompressed size 4 bytes - $entry->getSize(), - // file name length 2 bytes - strlen($entry->getName()), - // extra field length 2 bytes - $extraSize, - // file comment length 2 bytes - $commentLength, - // disk number start 2 bytes - 0, - // internal file attributes 2 bytes - 0, - // external file attributes 4 bytes - $entry->getExternalAttributes(), - // relative offset of local header 4 bytes - $entry->getOffset() - ) - ); - // file name (variable size) - fwrite($outputStream, $entry->getName()); - if (0 < $extraSize) { - // extra field (variable size) - fwrite($outputStream, $extra); - } - if (0 < $commentLength) { - // file comment (variable size) - fwrite($outputStream, $entry->getComment()); - } - return true; - } - - public function release() - { - unset($this->entries); - unset($this->modifiedEntries); - } - - function __destruct() - { - $this->release(); - } - -} \ No newline at end of file diff --git a/src/PhpZip/Model/EndOfCentralDirectory.php b/src/PhpZip/Model/EndOfCentralDirectory.php index 1730c0c..8016e5f 100644 --- a/src/PhpZip/Model/EndOfCentralDirectory.php +++ b/src/PhpZip/Model/EndOfCentralDirectory.php @@ -1,11 +1,6 @@ mapper = new PositionMapper(); - } - - /** - * Positions the file pointer at the first Central File Header. - * Performs some means to check that this is really a ZIP file. - * - * @param resource $inputStream - * @throws ZipException If the file is not compatible to the ZIP File - * Format Specification. - */ - public function findCentralDirectory($inputStream) - { - // Search for End of central directory record. - $stats = fstat($inputStream); - $size = $stats['size']; - $max = $size - self::END_OF_CENTRAL_DIRECTORY_RECORD_MIN_LEN; - $min = $max >= 0xffff ? $max - 0xffff : 0; - for ($endOfCentralDirRecordPos = $max; $endOfCentralDirRecordPos >= $min; $endOfCentralDirRecordPos--) { - fseek($inputStream, $endOfCentralDirRecordPos, SEEK_SET); - // end of central dir signature 4 bytes (0x06054b50) - if (self::END_OF_CENTRAL_DIRECTORY_RECORD_SIG !== unpack('V', fread($inputStream, 4))[1]) - continue; - - // number of this disk - 2 bytes - // number of the disk with the start of the - // central directory - 2 bytes - // total number of entries in the central - // directory on this disk - 2 bytes - // total number of entries in the central - // directory - 2 bytes - // size of the central directory - 4 bytes - // offset of start of central directory with - // respect to the starting disk number - 4 bytes - // ZIP file comment length - 2 bytes - $data = unpack( - 'vdiskNo/vcdDiskNo/vcdEntriesDisk/vcdEntries/VcdSize/VcdPos/vcommentLength', - fread($inputStream, 18) - ); - - if (0 !== $data['diskNo'] || 0 !== $data['cdDiskNo'] || $data['cdEntriesDisk'] !== $data['cdEntries']) { - throw new ZipException( - "ZIP file spanning/splitting is not supported!" - ); - } - // .ZIP file comment (variable size) - if (0 < $data['commentLength']) { - $this->comment = fread($inputStream, $data['commentLength']); - } - $this->preamble = $endOfCentralDirRecordPos; - $this->postamble = $size - ftell($inputStream); - - // Check for ZIP64 End Of Central Directory Locator. - $endOfCentralDirLocatorPos = $endOfCentralDirRecordPos - self::ZIP64_END_OF_CENTRAL_DIRECTORY_LOCATOR_LEN; - - fseek($inputStream, $endOfCentralDirLocatorPos, SEEK_SET); - // zip64 end of central dir locator - // signature 4 bytes (0x07064b50) - if ( - 0 > $endOfCentralDirLocatorPos || - ftell($inputStream) === $size || - self::ZIP64_END_OF_CENTRAL_DIRECTORY_LOCATOR_SIG !== unpack('V', fread($inputStream, 4))[1] - ) { - // Seek and check first CFH, probably requiring an offset mapper. - $offset = $endOfCentralDirRecordPos - $data['cdSize']; - fseek($inputStream, $offset, SEEK_SET); - $offset -= $data['cdPos']; - if (0 !== $offset) { - $this->mapper = new OffsetPositionMapper($offset); - } - $this->centralDirectoryEntriesSize = $data['cdEntries']; - return; - } - - // number of the disk with the - // start of the zip64 end of - // central directory 4 bytes - $zip64EndOfCentralDirectoryRecordDisk = unpack('V', fread($inputStream, 4))[1]; - // relative offset of the zip64 - // end of central directory record 8 bytes - $zip64EndOfCentralDirectoryRecordPos = PackUtil::unpackLongLE(fread($inputStream, 8)); - // total number of disks 4 bytes - $totalDisks = unpack('V', fread($inputStream, 4))[1]; - if (0 !== $zip64EndOfCentralDirectoryRecordDisk || 1 !== $totalDisks) { - throw new ZipException("ZIP file spanning/splitting is not supported!"); - } - fseek($inputStream, $zip64EndOfCentralDirectoryRecordPos, SEEK_SET); - // zip64 end of central dir - // signature 4 bytes (0x06064b50) - $zip64EndOfCentralDirSig = unpack('V', fread($inputStream, 4))[1]; - if (self::ZIP64_END_OF_CENTRAL_DIRECTORY_RECORD_SIG !== $zip64EndOfCentralDirSig) { - throw new ZipException("Expected ZIP64 End Of Central Directory Record!"); - } - // size of zip64 end of central - // directory record 8 bytes - // version made by 2 bytes - // version needed to extract 2 bytes - fseek($inputStream, 12, SEEK_CUR); - // number of this disk 4 bytes - $diskNo = unpack('V', fread($inputStream, 4))[1]; - // number of the disk with the - // start of the central directory 4 bytes - $cdDiskNo = unpack('V', fread($inputStream, 4))[1]; - // total number of entries in the - // central directory on this disk 8 bytes - $cdEntriesDisk = PackUtil::unpackLongLE(fread($inputStream, 8)); - // total number of entries in the - // central directory 8 bytes - $cdEntries = PackUtil::unpackLongLE(fread($inputStream, 8)); - if (0 !== $diskNo || 0 !== $cdDiskNo || $cdEntriesDisk !== $cdEntries) { - throw new ZipException("ZIP file spanning/splitting is not supported!"); - } - if ($cdEntries < 0 || 0x7fffffff < $cdEntries) { - throw new ZipException("Total Number Of Entries In The Central Directory out of range!"); - } - // size of the central directory 8 bytes - fseek($inputStream, 8, SEEK_CUR); - // offset of start of central - // directory with respect to - // the starting disk number 8 bytes - $cdPos = PackUtil::unpackLongLE(fread($inputStream, 8)); - // zip64 extensible data sector (variable size) - fseek($inputStream, $cdPos, SEEK_SET); - $this->preamble = $zip64EndOfCentralDirectoryRecordPos; - $this->centralDirectoryEntriesSize = $cdEntries; - $this->zip64 = true; - return; - } - // Start recovering file entries from min. - $this->preamble = $min; - $this->postamble = $size - $min; - $this->centralDirectoryEntriesSize = 0; + $this->entryCount = $entryCount; + $this->comment = $comment; + $this->zip64 = $zip64; } /** @@ -256,9 +105,9 @@ class EndOfCentralDirectory /** * @return int */ - public function getCentralDirectoryEntriesSize() + public function getEntryCount() { - return $this->centralDirectoryEntriesSize; + return $this->entryCount; } /** @@ -268,152 +117,4 @@ class EndOfCentralDirectory { return $this->zip64; } - - /** - * @return int - */ - public function getPreamble() - { - return $this->preamble; - } - - /** - * @return int - */ - public function getPostamble() - { - return $this->postamble; - } - - /** - * @return PositionMapper - */ - public function getMapper() - { - return $this->mapper; - } - - /** - * @param int $preamble - */ - public function setPreamble($preamble) - { - $this->preamble = $preamble; - } - - /** - * Set archive comment - * @param string|null $comment - * @throws InvalidArgumentException - */ - public function setComment($comment = null) - { - if (null !== $comment && strlen($comment) !== 0) { - $comment = (string)$comment; - $length = strlen($comment); - if (0x0000 > $length || $length > 0xffff) { - throw new InvalidArgumentException('Length comment out of range'); - } - } - $this->modified = $comment !== $this->comment; - $this->newComment = $comment; - } - - /** - * Write end of central directory. - * - * @param resource $outputStream Output stream - * @param int $centralDirectoryEntries Size entries - * @param int $centralDirectoryOffset Offset central directory - */ - public function writeEndOfCentralDirectory($outputStream, $centralDirectoryEntries, $centralDirectoryOffset) - { - $position = ftell($outputStream); - $centralDirectorySize = $position - $centralDirectoryOffset; - $centralDirectoryEntriesZip64 = $centralDirectoryEntries > 0xffff; - $centralDirectorySizeZip64 = $centralDirectorySize > 0xffffffff; - $centralDirectoryOffsetZip64 = $centralDirectoryOffset > 0xffffffff; - $centralDirectoryEntries16 = $centralDirectoryEntriesZip64 ? 0xffff : (int)$centralDirectoryEntries; - $centralDirectorySize32 = $centralDirectorySizeZip64 ? 0xffffffff : $centralDirectorySize; - $centralDirectoryOffset32 = $centralDirectoryOffsetZip64 ? 0xffffffff : $centralDirectoryOffset; - $zip64 // ZIP64 extensions? - = $centralDirectoryEntriesZip64 - || $centralDirectorySizeZip64 - || $centralDirectoryOffsetZip64; - if ($zip64) { - // relative offset of the zip64 end of central directory record - $zip64EndOfCentralDirectoryOffset = $position; - // zip64 end of central dir - // signature 4 bytes (0x06064b50) - fwrite($outputStream, pack('V', self::ZIP64_END_OF_CENTRAL_DIRECTORY_RECORD_SIG)); - // size of zip64 end of central - // directory record 8 bytes - fwrite($outputStream, PackUtil::packLongLE(self::ZIP64_END_OF_CENTRAL_DIRECTORY_RECORD_MIN_LEN - 12)); - // version made by 2 bytes - // version needed to extract 2 bytes - // due to potential use of BZIP2 compression - // number of this disk 4 bytes - // number of the disk with the - // start of the central directory 4 bytes - fwrite($outputStream, pack('vvVV', 63, 46, 0, 0)); - // total number of entries in the - // central directory on this disk 8 bytes - fwrite($outputStream, PackUtil::packLongLE($centralDirectoryEntries)); - // total number of entries in the - // central directory 8 bytes - fwrite($outputStream, PackUtil::packLongLE($centralDirectoryEntries)); - // size of the central directory 8 bytes - fwrite($outputStream, PackUtil::packLongLE($centralDirectorySize)); - // offset of start of central - // directory with respect to - // the starting disk number 8 bytes - fwrite($outputStream, PackUtil::packLongLE($centralDirectoryOffset)); - // zip64 extensible data sector (variable size) - // - // zip64 end of central dir locator - // signature 4 bytes (0x07064b50) - // number of the disk with the - // start of the zip64 end of - // central directory 4 bytes - fwrite($outputStream, pack('VV', self::ZIP64_END_OF_CENTRAL_DIRECTORY_LOCATOR_SIG, 0)); - // relative offset of the zip64 - // end of central directory record 8 bytes - fwrite($outputStream, PackUtil::packLongLE($zip64EndOfCentralDirectoryOffset)); - // total number of disks 4 bytes - fwrite($outputStream, pack('V', 1)); - } - $comment = $this->modified ? $this->newComment : $this->comment; - $commentLength = strlen($comment); - fwrite( - $outputStream, - pack('VvvvvVVv', - // end of central dir signature 4 bytes (0x06054b50) - self::END_OF_CENTRAL_DIRECTORY_RECORD_SIG, - // number of this disk 2 bytes - 0, - // number of the disk with the - // start of the central directory 2 bytes - 0, - // total number of entries in the - // central directory on this disk 2 bytes - $centralDirectoryEntries16, - // total number of entries in - // the central directory 2 bytes - $centralDirectoryEntries16, - // size of the central directory 4 bytes - $centralDirectorySize32, - // offset of start of central - // directory with respect to - // the starting disk number 4 bytes - $centralDirectoryOffset32, - // .ZIP file comment length 2 bytes - $commentLength - ) - ); - if ($commentLength > 0) { - // .ZIP file comment (variable size) - fwrite($outputStream, $comment); - } - } - -} \ No newline at end of file +} diff --git a/src/PhpZip/Model/Entry/OutputOffsetEntry.php b/src/PhpZip/Model/Entry/OutputOffsetEntry.php new file mode 100644 index 0000000..94bd15b --- /dev/null +++ b/src/PhpZip/Model/Entry/OutputOffsetEntry.php @@ -0,0 +1,49 @@ +offset = $pos; + $this->entry = $entry; + } + + /** + * @return int + */ + public function getOffset() + { + return $this->offset; + } + + /** + * @return ZipEntry + */ + public function getEntry() + { + return $this->entry; + } +} diff --git a/src/PhpZip/Model/Entry/ZipAbstractEntry.php b/src/PhpZip/Model/Entry/ZipAbstractEntry.php index 17f8d24..396d234 100644 --- a/src/PhpZip/Model/Entry/ZipAbstractEntry.php +++ b/src/PhpZip/Model/Entry/ZipAbstractEntry.php @@ -4,15 +4,14 @@ namespace PhpZip\Model\Entry; use PhpZip\Exception\InvalidArgumentException; use PhpZip\Exception\ZipException; -use PhpZip\Extra\DefaultExtraField; -use PhpZip\Extra\ExtraField; -use PhpZip\Extra\ExtraFields; -use PhpZip\Extra\WinZipAesEntryExtraField; -use PhpZip\Model\CentralDirectory; +use PhpZip\Extra\ExtraFieldsCollection; +use PhpZip\Extra\ExtraFieldsFactory; +use PhpZip\Extra\Fields\WinZipAesEntryExtraField; +use PhpZip\Extra\Fields\Zip64ExtraField; use PhpZip\Model\ZipEntry; use PhpZip\Util\DateTimeConverter; -use PhpZip\Util\PackUtil; -use PhpZip\ZipFile; +use PhpZip\Util\StringUtil; +use PhpZip\ZipFileInterface; /** * Abstract ZIP entry. @@ -23,16 +22,10 @@ use PhpZip\ZipFile; */ abstract class ZipAbstractEntry implements ZipEntry { - /** - * @var CentralDirectory - */ - private $centralDirectory; - /** * @var int Bit flags for init state. */ private $init; - /** * @var string Entry name (filename in archive) */ @@ -45,14 +38,14 @@ abstract class ZipAbstractEntry implements ZipEntry * @var int */ private $versionNeededToExtract = 20; - /** - * @var int - */ - private $general; /** * @var int Compression method */ private $method; + /** + * @var int + */ + private $general; /** * @var int Dos time */ @@ -78,13 +71,13 @@ abstract class ZipAbstractEntry implements ZipEntry */ private $offset = self::UNKNOWN; /** - * The map of Extra Fields. - * Maps from Header ID [Integer] to Extra Field [ExtraField]. + * Collections of Extra Fields. + * Keys from Header ID [int] and value Extra Field [ExtraField]. * Should be null or may be empty if no Extra Fields are used. * - * @var ExtraFields + * @var ExtraFieldsCollection */ - private $fields; + private $extraFieldsCollection; /** * @var string Comment field. */ @@ -95,55 +88,48 @@ abstract class ZipAbstractEntry implements ZipEntry private $password; /** * Encryption method. - * @see ZipFile::ENCRYPTION_METHOD_TRADITIONAL - * @see ZipFile::ENCRYPTION_METHOD_WINZIP_AES + * @see ZipFileInterface::ENCRYPTION_METHOD_TRADITIONAL + * @see ZipFileInterface::ENCRYPTION_METHOD_WINZIP_AES_128 + * @see ZipFileInterface::ENCRYPTION_METHOD_WINZIP_AES_192 + * @see ZipFileInterface::ENCRYPTION_METHOD_WINZIP_AES_256 * @var int */ - private $encryptionMethod = ZipFile::ENCRYPTION_METHOD_TRADITIONAL; - + private $encryptionMethod = ZipFileInterface::ENCRYPTION_METHOD_TRADITIONAL; /** * @var int */ - private $compressionLevel = ZipFile::LEVEL_DEFAULT_COMPRESSION; + private $compressionLevel = ZipFileInterface::LEVEL_DEFAULT_COMPRESSION; /** - * @param int $mask - * @return bool + * ZipAbstractEntry constructor. */ - private function isInit($mask) + public function __construct() { - return 0 !== ($this->init & $mask); + $this->extraFieldsCollection = new ExtraFieldsCollection(); } /** - * @param int $mask - * @param bool $init + * @param ZipEntry $entry */ - private function setInit($mask, $init) + public function setEntry(ZipEntry $entry) { - if ($init) { - $this->init |= $mask; - } else { - $this->init &= ~$mask; - } - } - - /** - * @return CentralDirectory - */ - public function getCentralDirectory() - { - return $this->centralDirectory; - } - - /** - * @param CentralDirectory $centralDirectory - * @return ZipEntry - */ - public function setCentralDirectory(CentralDirectory $centralDirectory) - { - $this->centralDirectory = $centralDirectory; - return $this; + $this->setName($entry->getName()); + $this->setPlatform($entry->getPlatform()); + $this->setVersionNeededToExtract($entry->getVersionNeededToExtract()); + $this->setMethod($entry->getMethod()); + $this->setGeneralPurposeBitFlags($entry->getGeneralPurposeBitFlags()); + $this->setDosTime($entry->getDosTime()); + $this->setCrc($entry->getCrc()); + $this->setCompressedSize($entry->getCompressedSize()); + $this->setSize($entry->getSize()); + $this->setExternalAttributes($entry->getExternalAttributes()); + $this->setOffset($entry->getOffset()); + $this->setExtra($entry->getExtra()); + $this->setComment($entry->getComment()); + $this->setPassword($entry->getPassword()); + $this->setEncryptionMethod($entry->getEncryptionMethod()); + $this->setCompressionLevel($entry->getCompressionLevel()); + $this->setEncrypted($entry->isEncrypted()); } /** @@ -174,6 +160,23 @@ abstract class ZipAbstractEntry implements ZipEntry return $this; } + /** + * Sets the indexed General Purpose Bit Flag. + * + * @param int $mask + * @param bool $bit + * @return ZipEntry + */ + public function setGeneralPurposeBitFlag($mask, $bit) + { + if ($bit) { + $this->general |= $mask; + } else { + $this->general &= ~$mask; + } + return $this; + } + /** * @return int Get platform */ @@ -204,6 +207,28 @@ abstract class ZipAbstractEntry implements ZipEntry return $this; } + /** + * @param int $mask + * @return bool + */ + protected function isInit($mask) + { + return 0 !== ($this->init & $mask); + } + + /** + * @param int $mask + * @param bool $init + */ + protected function setInit($mask, $init) + { + if ($init) { + $this->init |= $mask; + } else { + $this->init &= ~$mask; + } + } + /** * Version needed to extract. * @@ -235,7 +260,7 @@ abstract class ZipAbstractEntry implements ZipEntry // description of Data Descriptor in ZIP File Format Specification! return 0xffffffff <= $this->getCompressedSize() || 0xffffffff <= $this->getSize() - || 0xffffffff <= $this->getOffset(); + || 0xffffffff <= sprintf('%u', $this->getOffset()); } /** @@ -257,12 +282,6 @@ abstract class ZipAbstractEntry implements ZipEntry */ public function setCompressedSize($compressedSize) { - if (self::UNKNOWN != $compressedSize) { - $compressedSize = sprintf('%u', $compressedSize); - if (0 > $compressedSize || $compressedSize > 0x7fffffffffffffff) { - throw new ZipException("Compressed size out of range - " . $this->name); - } - } $this->compressedSize = $compressedSize; return $this; } @@ -286,12 +305,6 @@ abstract class ZipAbstractEntry implements ZipEntry */ public function setSize($size) { - if (self::UNKNOWN != $size) { - $size = sprintf('%u', $size); - if (0 > $size || $size > 0x7fffffffffffffff) { - throw new ZipException("Uncompressed Size out of range - " . $this->name); - } - } $this->size = $size; return $this; } @@ -313,29 +326,13 @@ abstract class ZipAbstractEntry implements ZipEntry */ public function setOffset($offset) { - $offset = sprintf('%u', $offset); - if (0 > $offset || $offset > 0x7fffffffffffffff) { - throw new ZipException("Offset out of range - " . $this->name); - } $this->offset = $offset; return $this; } - /** - * Returns true if and only if this ZIP entry represents a directory entry - * (i.e. end with '/'). - * - * @return bool - */ - public function isDirectory() - { - return $this->name[strlen($this->name) - 1] === '/'; - } - /** * Returns the General Purpose Bit Flags. - * - * @return bool + * @return int */ public function getGeneralPurposeBitFlags() { @@ -355,33 +352,19 @@ abstract class ZipAbstractEntry implements ZipEntry throw new ZipException('general out of range'); } $this->general = $general; - return $this; - } - - /** - * Returns the indexed General Purpose Bit Flag. - * - * @param int $mask - * @return bool - */ - public function getGeneralPurposeBitFlag($mask) - { - return 0 !== ($this->general & $mask); - } - - /** - * Sets the indexed General Purpose Bit Flag. - * - * @param int $mask - * @param bool $bit - * @return ZipEntry - */ - public function setGeneralPurposeBitFlag($mask, $bit) - { - if ($bit) - $this->general |= $mask; - else - $this->general &= ~$mask; + if ($this->method === ZipFileInterface::METHOD_DEFLATED) { + $bit1 = $this->getGeneralPurposeBitFlag(self::GPBF_COMPRESSION_FLAG1); + $bit2 = $this->getGeneralPurposeBitFlag(self::GPBF_COMPRESSION_FLAG2); + if ($bit1 && !$bit2) { + $this->compressionLevel = ZipFileInterface::LEVEL_BEST_COMPRESSION; + } elseif (!$bit1 && $bit2) { + $this->compressionLevel = ZipFileInterface::LEVEL_FAST; + } elseif ($bit1 && $bit2) { + $this->compressionLevel = ZipFileInterface::LEVEL_SUPER_FAST; + } else { + $this->compressionLevel = ZipFileInterface::LEVEL_DEFAULT_COMPRESSION; + } + } return $this; } @@ -395,26 +378,36 @@ abstract class ZipAbstractEntry implements ZipEntry return $this->getGeneralPurposeBitFlag(self::GPBF_ENCRYPTED); } + /** + * Returns the indexed General Purpose Bit Flag. + * + * @param int $mask + * @return bool + */ + public function getGeneralPurposeBitFlag($mask) + { + return 0 !== ($this->general & $mask); + } + /** * Sets the encryption property to false and removes any other * encryption artifacts. * * @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 @@ abstract class ZipAbstractEntry implements ZipEntry */ public function setEncrypted($encrypted) { + $encrypted = (bool)$encrypted; $this->setGeneralPurposeBitFlag(self::GPBF_ENCRYPTED, $encrypted); return $this; } @@ -451,6 +445,10 @@ abstract class ZipAbstractEntry implements ZipEntry */ public function setMethod($method) { + if (self::UNKNOWN === $method) { + $this->method = $method; + return $this; + } if (0x0000 > $method || $method > 0xffff) { throw new ZipException('method out of range'); } @@ -458,21 +456,15 @@ abstract class ZipAbstractEntry implements ZipEntry case self::METHOD_WINZIP_AES: $this->method = $method; $this->setInit(self::BIT_METHOD, true); - $this->setEncryptionMethod(ZipFile::ENCRYPTION_METHOD_WINZIP_AES); break; - case ZipFile::METHOD_STORED: - case ZipFile::METHOD_DEFLATED: - case ZipFile::METHOD_BZIP2: + case ZipFileInterface::METHOD_STORED: + case ZipFileInterface::METHOD_DEFLATED: + case ZipFileInterface::METHOD_BZIP2: $this->method = $method; $this->setInit(self::BIT_METHOD, true); break; - case self::UNKNOWN: - $this->method = ZipFile::METHOD_STORED; - $this->setInit(self::BIT_METHOD, false); - break; - default: throw new ZipException($this->name . " (unsupported compression method $method)"); } @@ -492,24 +484,6 @@ abstract class ZipAbstractEntry implements ZipEntry return DateTimeConverter::toUnixTimestamp($this->getDosTime()); } - /** - * Set time from unix timestamp. - * - * @param int $unixTimestamp - * @return ZipEntry - */ - public function setTime($unixTimestamp) - { - $known = self::UNKNOWN != $unixTimestamp; - if ($known) { - $this->dosTime = DateTimeConverter::toDosTime($unixTimestamp); - } else { - $this->dosTime = 0; - } - $this->setInit(self::BIT_DATE_TIME, $known); - return $this; - } - /** * Get Dos Time * @@ -517,7 +491,7 @@ abstract class ZipAbstractEntry implements ZipEntry */ public function getDosTime() { - return $this->dosTime & 0xffffffff; + return $this->dosTime; } /** @@ -535,6 +509,24 @@ abstract class ZipAbstractEntry implements ZipEntry $this->setInit(self::BIT_DATE_TIME, true); } + /** + * Set time from unix timestamp. + * + * @param int $unixTimestamp + * @return ZipEntry + */ + public function setTime($unixTimestamp) + { + $known = self::UNKNOWN != $unixTimestamp; + if ($known) { + $this->dosTime = DateTimeConverter::toDosTime($unixTimestamp); + } else { + $this->dosTime = 0; + } + $this->setInit(self::BIT_DATE_TIME, $known); + return $this; + } + /** * Returns the external file attributes. * @@ -545,7 +537,7 @@ abstract class ZipAbstractEntry implements ZipEntry if (!$this->isInit(self::BIT_EXTERNAL_ATTR)) { return $this->isDirectory() ? 0x10 : 0; } - return $this->externalAttributes & 0xffffffff; + return $this->externalAttributes; } /** @@ -559,10 +551,6 @@ abstract class ZipAbstractEntry implements ZipEntry { $known = self::UNKNOWN != $externalAttributes; if ($known) { - $externalAttributes = sprintf('%u', $externalAttributes); - if (0x00000000 > $externalAttributes || $externalAttributes > 0xffffffff) { - throw new ZipException("external file attributes out of range - " . $this->name); - } $this->externalAttributes = $externalAttributes; } else { $this->externalAttributes = 0; @@ -572,123 +560,43 @@ abstract class ZipAbstractEntry implements ZipEntry } /** - * Return extra field from header id. + * Returns true if and only if this ZIP entry represents a directory entry + * (i.e. end with '/'). * - * @param int $headerId - * @return ExtraField|null - */ - public function getExtraField($headerId) - { - return $this->fields === null ? null : $this->fields->get($headerId); - } - - /** - * Add extra field. - * - * @param ExtraField $field - * @return ExtraField - * @throws ZipException - */ - public function addExtraField($field) - { - if (null === $field) { - throw new ZipException("extra field null"); - } - if (null === $this->fields) { - $this->fields = new ExtraFields(); - } - return $this->fields->add($field); - } - - /** - * Return exists extra field from header id. - * - * @param int $headerId * @return bool */ - public function hasExtraField($headerId) + public function isDirectory() { - return $this->fields === null ? false : $this->fields->has($headerId); + return StringUtil::endsWith($this->name, '/'); } /** - * Remove extra field from header id. - * - * @param int $headerId - * @return ExtraField|null + * @return ExtraFieldsCollection */ - public function removeExtraField($headerId) + public function &getExtraFieldsCollection() { - return null !== $this->fields ? $this->fields->remove($headerId) : null; + return $this->extraFieldsCollection; } /** * Returns a protective copy of the serialized Extra Fields. - * - * @return string A new byte array holding the serialized Extra Fields. - * null is never returned. - */ - public function getExtra() - { - return $this->getExtraFields(false); - } - - /** - * @param bool $zip64 * @return string * @throws ZipException */ - private function getExtraFields($zip64) + public function getExtra() { - if ($zip64) { - $field = $this->composeZip64ExtraField(); - if (null !== $field) { - if (null === $this->fields) { - $this->fields = new ExtraFields(); - } - $this->fields->add($field); - } - } else { - assert(null === $this->fields || null === $this->fields->get(ExtraField::ZIP64_HEADER_ID)); + $extraData = ''; + foreach ($this->getExtraFieldsCollection() as $extraField) { + $data = $extraField->serialize(); + $extraData .= pack('vv', $extraField::getHeaderId(), strlen($data)); + $extraData .= $data; } - return null === $this->fields ? null : $this->fields->getExtra(); - } - /** - * Composes a ZIP64 Extended Information Extra Field from the properties - * of this entry. - * If no ZIP64 Extended Information Extra Field is required it is removed - * from the collection of Extra Fields. - * - * @return ExtraField|null - */ - private function composeZip64ExtraField() - { - $handle = fopen('php://memory', 'r+b'); - // Write out Uncompressed Size. - $size = $this->getSize(); - if (0xffffffff <= $size) { - fwrite($handle, PackUtil::packLongLE($size)); + $size = strlen($extraData); + if (0x0000 > $size || $size > 0xffff) { + throw new ZipException('Size extra out of range: ' . $size . '. Extra data: ' . $extraData); } - // Write out Compressed Size. - $compressedSize = $this->getCompressedSize(); - if (0xffffffff <= $compressedSize) { - fwrite($handle, PackUtil::packLongLE($compressedSize)); - } - // Write out Relative Header Offset. - $offset = $this->getOffset(); - if (0xffffffff <= $offset) { - fwrite($handle, PackUtil::packLongLE($offset)); - } - // Create ZIP64 Extended Information Extra Field from serialized data. - $field = null; - if (ftell($handle) > 0) { - $field = new DefaultExtraField(ExtraField::ZIP64_HEADER_ID); - $field->readFrom($handle, 0, ftell($handle)); - } else { - $field = null; - } - return $field; + return $extraData; } /** @@ -701,99 +609,31 @@ abstract class ZipAbstractEntry implements ZipEntry * * @param string $data The byte array holding the serialized Extra Fields. * @throws ZipException if the serialized Extra Fields exceed 64 KB - * @return ZipEntry - * or do not conform to the ZIP File Format Specification */ public function setExtra($data) { + $this->extraFieldsCollection = new ExtraFieldsCollection(); if (null !== $data) { - $length = strlen($data); - if (0x0000 > $length || $length > 0xffff) { - throw new ZipException("Extra Fields too large"); + $extraLength = strlen($data); + if (0x0000 > $extraLength || $extraLength > 0xffff) { + throw new ZipException("Extra Fields too large: " . $extraLength); } - } - if (null === $data || strlen($data) <= 0) { - $this->fields = null; - } else { - $this->setExtraFields($data, false); - } - return $this; - } - - /** - * @param string $data - * @param bool $zip64 - */ - private function setExtraFields($data, $zip64) - { - if (null === $this->fields) { - $this->fields = new ExtraFields(); - } - $handle = fopen('php://memory', 'r+b'); - fwrite($handle, $data); - rewind($handle); - - $this->fields->readFrom($handle, 0, strlen($data)); - $result = false; - if ($zip64) { - $result = $this->parseZip64ExtraField(); - } - if ($result) { - $this->fields->remove(ExtraField::ZIP64_HEADER_ID); - if ($this->fields->size() <= 0) { - if (0 !== $this->fields->size()) { - $this->fields = null; + $pos = 0; + $endPos = $extraLength; + while ($pos < $endPos) { + $unpack = unpack('vheaderId/vdataSize', substr($data, $pos, 4)); + $pos += 4; + $headerId = (int)$unpack['headerId']; + $dataSize = (int)$unpack['dataSize']; + $extraField = ExtraFieldsFactory::create($headerId); + if ($extraField instanceof Zip64ExtraField) { + $extraField->setEntry($this); } + $extraField->deserialize(substr($data, $pos, $dataSize)); + $pos += $dataSize; + $this->extraFieldsCollection[$headerId] = $extraField; } } - fclose($handle); - } - - /** - * Parses the properties of this entry from the ZIP64 Extended Information - * Extra Field, if present. - * The ZIP64 Extended Information Extra Field is not removed. - * - * @return bool - * @throws ZipException - */ - private function parseZip64ExtraField() - { - if (null === $this->fields) { - return false; - } - $ef = $this->fields->get(ExtraField::ZIP64_HEADER_ID); - if (null === $ef) { - return false; - } - $dataBlockHandle = $ef->getDataBlock(); - $off = 0; - // Read in Uncompressed Size. - $size = $this->getSize(); - if (0xffffffff <= $size) { - assert(0xffffffff === $size); - fseek($dataBlockHandle, $off); - $this->setSize(PackUtil::unpackLongLE(fread($dataBlockHandle, 8))); - $off += 8; - } - // Read in Compressed Size. - $compressedSize = $this->getCompressedSize(); - if (0xffffffff <= $compressedSize) { - assert(0xffffffff === $compressedSize); - fseek($dataBlockHandle, $off); - $this->setCompressedSize(PackUtil::unpackLongLE(fread($dataBlockHandle, 8))); - $off += 8; - } - // Read in Relative Header Offset. - $offset = $this->getOffset(); - if (0xffffffff <= $offset) { - assert(0xffffffff, $offset); - fseek($dataBlockHandle, $off); - $this->setOffset(PackUtil::unpackLongLE(fread($dataBlockHandle, 8))); - //$off += 8; - } - fclose($dataBlockHandle); - return true; } /** @@ -803,7 +643,7 @@ abstract class ZipAbstractEntry implements ZipEntry */ public function getComment() { - return null != $this->comment ? $this->comment : ""; + return null !== $this->comment ? $this->comment : ""; } /** @@ -841,7 +681,7 @@ abstract class ZipAbstractEntry implements ZipEntry */ public function getCrc() { - return $this->crc & 0xffffffff; + return $this->crc; } /** @@ -853,10 +693,6 @@ abstract class ZipAbstractEntry implements ZipEntry */ public function setCrc($crc) { - $crc = sprintf('%u', $crc); - if (0x00000000 > $crc || $crc > 0xffffffff) { - throw new ZipException("CRC-32 out of range - " . $this->name); - } $this->crc = $crc; $this->setInit(self::BIT_CRC, true); return $this; @@ -883,7 +719,11 @@ abstract class ZipAbstractEntry implements ZipEntry if (null !== $encryptionMethod) { $this->setEncryptionMethod($encryptionMethod); } - $this->setEncrypted(!empty($this->password)); + if (!empty($this->password)) { + $this->setEncrypted(true); + } else { + $this->disableEncryption(); + } return $this; } @@ -895,6 +735,34 @@ abstract class ZipAbstractEntry implements ZipEntry return $this->encryptionMethod; } + /** + * Set encryption method + * + * @see ZipFileInterface::ENCRYPTION_METHOD_TRADITIONAL + * @see ZipFileInterface::ENCRYPTION_METHOD_WINZIP_AES_128 + * @see ZipFileInterface::ENCRYPTION_METHOD_WINZIP_AES_192 + * @see ZipFileInterface::ENCRYPTION_METHOD_WINZIP_AES_256 + * + * @param int $encryptionMethod + * @return ZipEntry + * @throws ZipException + */ + public function setEncryptionMethod($encryptionMethod) + { + if (null !== $encryptionMethod) { + if ( + ZipFileInterface::ENCRYPTION_METHOD_TRADITIONAL !== $encryptionMethod + && ZipFileInterface::ENCRYPTION_METHOD_WINZIP_AES_128 !== $encryptionMethod + && ZipFileInterface::ENCRYPTION_METHOD_WINZIP_AES_192 !== $encryptionMethod + && ZipFileInterface::ENCRYPTION_METHOD_WINZIP_AES_256 !== $encryptionMethod + ) { + throw new ZipException('Invalid encryption method'); + } + $this->encryptionMethod = $encryptionMethod; + } + return $this; + } + /** * @return int */ @@ -908,46 +776,23 @@ abstract class ZipAbstractEntry implements ZipEntry * @return ZipEntry * @throws InvalidArgumentException */ - public function setCompressionLevel($compressionLevel = ZipFile::LEVEL_DEFAULT_COMPRESSION) + public function setCompressionLevel($compressionLevel = ZipFileInterface::LEVEL_DEFAULT_COMPRESSION) { - if ($compressionLevel < ZipFile::LEVEL_DEFAULT_COMPRESSION || - $compressionLevel > ZipFile::LEVEL_BEST_COMPRESSION + if ($compressionLevel < ZipFileInterface::LEVEL_DEFAULT_COMPRESSION || + $compressionLevel > ZipFileInterface::LEVEL_BEST_COMPRESSION ) { throw new InvalidArgumentException('Invalid compression level. Minimum level ' . - ZipFile::LEVEL_DEFAULT_COMPRESSION . '. Maximum level ' . ZipFile::LEVEL_BEST_COMPRESSION); + ZipFileInterface::LEVEL_DEFAULT_COMPRESSION . '. Maximum level ' . ZipFileInterface::LEVEL_BEST_COMPRESSION); } $this->compressionLevel = $compressionLevel; return $this; } - /** - * Set encryption method - * - * @see ZipFile::ENCRYPTION_METHOD_TRADITIONAL - * @see ZipFile::ENCRYPTION_METHOD_WINZIP_AES - * - * @param int $encryptionMethod - * @return ZipEntry - * @throws ZipException - */ - public function setEncryptionMethod($encryptionMethod) - { - if ( - ZipFile::ENCRYPTION_METHOD_TRADITIONAL !== $encryptionMethod && - ZipFile::ENCRYPTION_METHOD_WINZIP_AES !== $encryptionMethod - ) { - throw new ZipException('Invalid encryption method'); - } - $this->encryptionMethod = $encryptionMethod; - $this->setEncrypted(true); - return $this; - } - /** * Clone extra fields */ - function __clone() + public function __clone() { - $this->fields = $this->fields !== null ? clone $this->fields : null; + $this->extraFieldsCollection = clone $this->extraFieldsCollection; } -} \ No newline at end of file +} diff --git a/src/PhpZip/Model/Entry/ZipChangesEntry.php b/src/PhpZip/Model/Entry/ZipChangesEntry.php new file mode 100644 index 0000000..205a793 --- /dev/null +++ b/src/PhpZip/Model/Entry/ZipChangesEntry.php @@ -0,0 +1,63 @@ +entry = $entry; + $this->setEntry($entry); + } + + /** + * @return bool + */ + public function isChangedContent() + { + return !( + $this->getCompressionLevel() === $this->entry->getCompressionLevel() && + $this->getMethod() === $this->entry->getMethod() && + $this->isEncrypted() === $this->entry->isEncrypted() && + $this->getEncryptionMethod() === $this->entry->getEncryptionMethod() && + $this->getPassword() === $this->entry->getPassword() + ); + } + + /** + * Returns an string content of the given entry. + * + * @return null|string + * @throws ZipException + */ + public function getEntryContent() + { + return $this->entry->getEntryContent(); + } + + /** + * @return ZipSourceEntry + */ + public function getSourceEntry() + { + return $this->entry; + } +} diff --git a/src/PhpZip/Model/Entry/ZipNewEmptyDirEntry.php b/src/PhpZip/Model/Entry/ZipNewEmptyDirEntry.php deleted file mode 100644 index de5f480..0000000 --- a/src/PhpZip/Model/Entry/ZipNewEmptyDirEntry.php +++ /dev/null @@ -1,26 +0,0 @@ -content = $content; + } + + /** + * Returns an string content of the given entry. + * + * @return null|string + * @throws ZipException + */ + public function getEntryContent() + { + if (is_resource($this->content)) { + return stream_get_contents($this->content, -1, 0); + } + return $this->content; + } /** * Version needed to extract. @@ -32,237 +61,29 @@ abstract class ZipNewEntry extends ZipAbstractEntry { $method = $this->getMethod(); return self::METHOD_WINZIP_AES === $method ? 51 : - (ZipFile::METHOD_BZIP2 === $method ? 46 : - ($this->isZip64ExtensionsRequired() ? 45 : - (ZipFile::METHOD_DEFLATED === $method || $this->isDirectory() ? 20 : 10) + ( + ZipFileInterface::METHOD_BZIP2 === $method ? 46 : + ( + $this->isZip64ExtensionsRequired() ? 45 : + (ZipFileInterface::METHOD_DEFLATED === $method || $this->isDirectory() ? 20 : 10) ) ); } /** - * Write local file header, encryption header, file data and data descriptor to output stream. - * - * @param resource $outputStream - * @throws ZipException + * Clone extra fields */ - public function writeEntry($outputStream) + public function __clone() { - $nameLength = strlen($this->getName()); - $size = $nameLength + strlen($this->getExtra()) + strlen($this->getComment()); - if (0xffff < $size) { - throw new ZipException($this->getName() - . " (the total size of " - . $size - . " bytes for the name, extra fields and comment exceeds the maximum size of " - . 0xffff . " bytes)"); - } - - if (self::UNKNOWN === $this->getPlatform()) { - $this->setPlatform(self::PLATFORM_UNIX); - } - if (self::UNKNOWN === $this->getTime()) { - $this->setTime(time()); - } - $method = $this->getMethod(); - if (self::UNKNOWN === $method) { - $this->setMethod($method = ZipFile::METHOD_DEFLATED); - } - $skipCrc = false; - - $encrypted = $this->isEncrypted(); - $dd = $this->isDataDescriptorRequired(); - // Compose General Purpose Bit Flag. - // See appendix D of PKWARE's ZIP File Format Specification. - $utf8 = true; - $general = ($encrypted ? self::GPBF_ENCRYPTED : 0) - | ($dd ? self::GPBF_DATA_DESCRIPTOR : 0) - | ($utf8 ? self::GPBF_UTF8 : 0); - - $entryContent = $this->getEntryContent(); - - $this->setSize(strlen($entryContent)); - $this->setCrc(crc32($entryContent)); - - if ($encrypted && null === $this->getPassword()) { - throw new ZipException("Can not password from entry " . $this->getName()); - } - - if ( - $encrypted && - ( - self::METHOD_WINZIP_AES === $method || - $this->getEncryptionMethod() === ZipFile::ENCRYPTION_METHOD_WINZIP_AES - ) - ) { - $field = null; - $method = $this->getMethod(); - $keyStrength = 256; // bits - - $compressedSize = $this->getCompressedSize(); - - if (self::METHOD_WINZIP_AES === $method) { - /** - * @var WinZipAesEntryExtraField $field - */ - $field = $this->getExtraField(WinZipAesEntryExtraField::getHeaderId()); - if (null !== $field) { - $method = $field->getMethod(); - if (self::UNKNOWN !== $compressedSize) { - $compressedSize -= $field->getKeyStrength() / 2 // salt value - + 2 // password verification value - + 10; // authentication code - } - $this->setMethod($method); - } - } - if (null === $field) { - $field = new WinZipAesEntryExtraField(); - } - $field->setKeyStrength($keyStrength); - $field->setMethod($method); - $size = $this->getSize(); - if (20 <= $size && ZipFile::METHOD_BZIP2 !== $method) { - $field->setVendorVersion(WinZipAesEntryExtraField::VV_AE_1); - } else { - $field->setVendorVersion(WinZipAesEntryExtraField::VV_AE_2); - $skipCrc = true; - } - $this->addExtraField($field); - if (self::UNKNOWN !== $compressedSize) { - $compressedSize += $field->getKeyStrength() / 2 // salt value - + 2 // password verification value - + 10; // authentication code - $this->setCompressedSize($compressedSize); - } - if ($skipCrc) { - $this->setCrc(0); - } - } - - switch ($method) { - case ZipFile::METHOD_STORED: - break; - case ZipFile::METHOD_DEFLATED: - $entryContent = gzdeflate($entryContent, $this->getCompressionLevel()); - break; - case ZipFile::METHOD_BZIP2: - $compressionLevel = $this->getCompressionLevel() === ZipFile::LEVEL_DEFAULT_COMPRESSION ? - self::LEVEL_DEFAULT_BZIP2_COMPRESSION : - $this->getCompressionLevel(); - $entryContent = bzcompress($entryContent, $compressionLevel); - if (is_int($entryContent)) { - throw new ZipException('Error bzip2 compress. Error code: ' . $entryContent); - } - break; - default: - throw new ZipException($this->getName() . " (unsupported compression method " . $method . ")"); - } - - if ($encrypted) { - if ($this->getEncryptionMethod() === ZipFile::ENCRYPTION_METHOD_WINZIP_AES) { - if ($skipCrc) { - $this->setCrc(0); - } - $this->setMethod(self::METHOD_WINZIP_AES); - - /** - * @var WinZipAesEntryExtraField $field - */ - $field = $this->getExtraField(WinZipAesEntryExtraField::getHeaderId()); - $winZipAesEngine = new WinZipAesEngine($this, $field); - $entryContent = $winZipAesEngine->encrypt($entryContent); - } elseif ($this->getEncryptionMethod() === ZipFile::ENCRYPTION_METHOD_TRADITIONAL) { - $zipCryptoEngine = new TraditionalPkwareEncryptionEngine($this); - $entryContent = $zipCryptoEngine->encrypt($entryContent); - } - } - - $compressedSize = strlen($entryContent); - $this->setCompressedSize($compressedSize); - - $offset = ftell($outputStream); - - // Commit changes. - $this->setGeneralPurposeBitFlags($general); - $this->setOffset($offset); - - $extra = $this->getExtra(); - - // zip align - $padding = 0; - $zipAlign = $this->getCentralDirectory()->getZipAlign(); - $extraLength = strlen($extra); - if ($zipAlign !== null && !$this->isEncrypted() && $this->getMethod() === ZipFile::METHOD_STORED) { - $padding = - ( - $zipAlign - - ( - $offset + - ZipEntry::LOCAL_FILE_HEADER_MIN_LEN + - $nameLength + $extraLength - ) % $zipAlign - ) % $zipAlign; - } - - fwrite( - $outputStream, - pack( - 'VvvvVVVVvv', - // local file header signature 4 bytes (0x04034b50) - self::LOCAL_FILE_HEADER_SIG, - // version needed to extract 2 bytes - $this->getVersionNeededToExtract(), - // general purpose bit flag 2 bytes - $general, - // compression method 2 bytes - $this->getMethod(), - // last mod file time 2 bytes - // last mod file date 2 bytes - $this->getDosTime(), - // crc-32 4 bytes - $dd ? 0 : $this->getCrc(), - // compressed size 4 bytes - $dd ? 0 : $this->getCompressedSize(), - // uncompressed size 4 bytes - $dd ? 0 : $this->getSize(), - // file name length 2 bytes - $nameLength, - // extra field length 2 bytes - $extraLength + $padding - ) - ); - fwrite($outputStream, $this->getName()); - if ($extraLength > 0) { - fwrite($outputStream, $extra); - } - - if ($padding > 0) { - fwrite($outputStream, str_repeat(chr(0), $padding)); - } - - if (null !== $entryContent) { - fwrite($outputStream, $entryContent); - } - - assert(self::UNKNOWN !== $this->getCrc()); - assert(self::UNKNOWN !== $this->getSize()); - if ($this->getGeneralPurposeBitFlag(self::GPBF_DATA_DESCRIPTOR)) { - // data descriptor signature 4 bytes (0x08074b50) - // crc-32 4 bytes - fwrite($outputStream, pack('VV', self::DATA_DESCRIPTOR_SIG, $this->getCrc())); - // compressed size 4 or 8 bytes - // uncompressed size 4 or 8 bytes - if ($this->isZip64ExtensionsRequired()) { - fwrite($outputStream, PackUtil::packLongLE($compressedSize)); - fwrite($outputStream, PackUtil::packLongLE($this->getSize())); - } else { - fwrite($outputStream, pack('VV', $this->getCompressedSize(), $this->getSize())); - } - } elseif ($this->getCompressedSize() != $compressedSize) { - throw new ZipException($this->getName() - . " (expected compressed entry size of " - . $this->getCompressedSize() . " bytes, but is actually " . $compressedSize . " bytes)"); - } + $this->clone = true; + parent::__clone(); } -} \ No newline at end of file + public function __destruct() + { + if (!$this->clone && null !== $this->content && is_resource($this->content)) { + fclose($this->content); + $this->content = null; + } + } +} diff --git a/src/PhpZip/Model/Entry/ZipNewStreamEntry.php b/src/PhpZip/Model/Entry/ZipNewStreamEntry.php deleted file mode 100644 index a8eb518..0000000 --- a/src/PhpZip/Model/Entry/ZipNewStreamEntry.php +++ /dev/null @@ -1,55 +0,0 @@ -stream = $stream; - } - - /** - * Returns an string content of the given entry. - * - * @return null|string - * @throws ZipException - */ - public function getEntryContent() - { - return stream_get_contents($this->stream, -1, 0); - } - - /** - * Release stream resource. - */ - function __destruct() - { - if (null !== $this->stream) { - fclose($this->stream); - $this->stream = null; - } - } -} \ No newline at end of file diff --git a/src/PhpZip/Model/Entry/ZipNewStringEntry.php b/src/PhpZip/Model/Entry/ZipNewStringEntry.php deleted file mode 100644 index d376957..0000000 --- a/src/PhpZip/Model/Entry/ZipNewStringEntry.php +++ /dev/null @@ -1,39 +0,0 @@ -entryContent = $entryContent; - } - - /** - * Returns an string content of the given entry. - * - * @return null|string - * @throws ZipException - */ - public function getEntryContent() - { - return $this->entryContent; - } -} \ No newline at end of file diff --git a/src/PhpZip/Model/Entry/ZipReadEntry.php b/src/PhpZip/Model/Entry/ZipReadEntry.php deleted file mode 100644 index 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 @@ class ZipInfo $atime = null; $ctime = null; - $field = $entry->getExtraField(NtfsExtraField::getHeaderId()); + $field = $entry->getExtraFieldsCollection()->get(NtfsExtraField::getHeaderId()); if (null !== $field && $field instanceof NtfsExtraField) { /** * @var NtfsExtraField $field */ $atime = $field->getAtime(); $ctime = $field->getCtime(); + $mtime = $field->getMtime(); } - $this->path = $entry->getName(); + $this->name = $entry->getName(); $this->folder = $entry->isDirectory(); - $this->size = $entry->getSize(); - $this->compressedSize = $entry->getCompressedSize(); + $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 @@ class ZipInfo if ($xattr & 0x10) { $attributes[0] = 'd'; $attributes[3] = 'x'; - } else + } else { $attributes[0] = '-'; - if ($xattr & 0x08) + } + if ($xattr & 0x08) { $attributes[0] = 'V'; - else { + } else { $ext = strtolower(pathinfo($entry->getName(), PATHINFO_EXTENSION)); if (in_array($ext, ["com", "exe", "btm", "cmd", "bat"])) { $attributes[3] = 'x'; @@ -250,6 +264,7 @@ class ZipInfo break; } /* else: fall through! */ + // no break default: /* assume Unix-like */ switch ($xattr & self::UNX_IFMT) { case self::UNX_IFDIR: @@ -284,33 +299,59 @@ class ZipInfo $attributes[5] = ($xattr & self::UNX_IWGRP) ? 'w' : '-'; $attributes[8] = ($xattr & self::UNX_IWOTH) ? 'w' : '-'; - if ($xattr & self::UNX_IXUSR) + if ($xattr & self::UNX_IXUSR) { $attributes[3] = ($xattr & self::UNX_ISUID) ? 's' : 'x'; - else - $attributes[3] = ($xattr & self::UNX_ISUID) ? 'S' : '-'; /* S==undefined */ - if ($xattr & self::UNX_IXGRP) - $attributes[6] = ($xattr & self::UNX_ISGID) ? 's' : 'x'; /* == UNX_ENFMT */ - else - $attributes[6] = ($xattr & self::UNX_ISGID) ? 'S' : '-'; /* SunOS 4.1.x */ - if ($xattr & self::UNX_IXOTH) - $attributes[9] = ($xattr & self::UNX_ISVTX) ? 't' : 'x'; /* "sticky bit" */ - else - $attributes[9] = ($xattr & self::UNX_ISVTX) ? 'T' : '-'; /* T==undefined */ + } else { + $attributes[3] = ($xattr & self::UNX_ISUID) ? 'S' : '-'; + } /* S==undefined */ + if ($xattr & self::UNX_IXGRP) { + $attributes[6] = ($xattr & self::UNX_ISGID) ? 's' : 'x'; + } /* == UNX_ENFMT */ + else { + $attributes[6] = ($xattr & self::UNX_ISGID) ? 'S' : '-'; + } /* SunOS 4.1.x */ + if ($xattr & self::UNX_IXOTH) { + $attributes[9] = ($xattr & self::UNX_ISVTX) ? 't' : 'x'; + } /* "sticky bit" */ + else { + $attributes[9] = ($xattr & self::UNX_ISVTX) ? 'T' : '-'; + } /* T==undefined */ } $this->attributes = trim($attributes); } + /** + * @param ZipEntry $entry + * @return int + */ + private static function getMethodId(ZipEntry $entry) + { + $method = $entry->getMethod(); + if ($entry->isEncrypted()) { + if ($entry->getMethod() === ZipEntry::METHOD_WINZIP_AES) { + $field = $entry->getExtraFieldsCollection()->get(WinZipAesEntryExtraField::getHeaderId()); + if (null !== $field) { + /** + * @var WinZipAesEntryExtraField $field + */ + $method = $field->getMethod(); + } + } + } + return $method; + } + /** * @param ZipEntry $entry * @return string */ - public static function getMethodName(ZipEntry $entry) + private static function getEntryMethodName(ZipEntry $entry) { $return = ''; if ($entry->isEncrypted()) { if ($entry->getMethod() === ZipEntry::METHOD_WINZIP_AES) { - $field = $entry->getExtraField(WinZipAesEntryExtraField::getHeaderId()); $return = ucfirst(self::$valuesCompressionMethod[$entry->getMethod()]); + $field = $entry->getExtraFieldsCollection()->get(WinZipAesEntryExtraField::getHeaderId()); if (null !== $field) { /** * @var WinZipAesEntryExtraField $field @@ -348,34 +389,20 @@ class ZipInfo } /** - * @return array + * @return string */ - public function toArray() + public function getName() { - return [ - 'path' => $this->getPath(), - 'folder' => $this->isFolder(), - 'size' => $this->getSize(), - 'compressed_size' => $this->getCompressedSize(), - 'modified' => $this->getMtime(), - 'created' => $this->getCtime(), - 'accessed' => $this->getAtime(), - 'attributes' => $this->getAttributes(), - 'encrypted' => $this->isEncrypted(), - 'comment' => $this->getComment(), - 'crc' => $this->getCrc(), - 'method' => $this->getMethod(), - 'platform' => $this->getPlatform(), - 'version' => $this->getVersion() - ]; + return $this->name; } /** * @return string + * @deprecated use \PhpZip\Model\ZipInfo::getName() */ public function getPath() { - return $this->path; + return $this->getName(); } /** @@ -426,6 +453,14 @@ class ZipInfo return $this->atime; } + /** + * @return string + */ + public function getAttributes() + { + return $this->attributes; + } + /** * @return boolean */ @@ -452,10 +487,19 @@ class ZipInfo /** * @return string + * @deprecated use \PhpZip\Model\ZipInfo::getMethodName() */ public function getMethod() { - return $this->method; + return $this->getMethodName(); + } + + /** + * @return string + */ + public function getMethodName() + { + return $this->methodName; } /** @@ -475,35 +519,76 @@ class ZipInfo } /** - * @return string + * @return int|null */ - public function getAttributes() + public function getEncryptionMethod() { - return $this->attributes; + return $this->encryptionMethod; + } + + /** + * @return int|null + */ + public function getCompressionLevel() + { + return $this->compressionLevel; + } + + /** + * @return int + */ + public function getCompressionMethod() + { + return $this->compressionMethod; + } + + /** + * @return array + */ + public function toArray() + { + return [ + 'name' => $this->getName(), + 'path' => $this->getName(), // deprecated + 'folder' => $this->isFolder(), + 'size' => $this->getSize(), + 'compressed_size' => $this->getCompressedSize(), + 'modified' => $this->getMtime(), + 'created' => $this->getCtime(), + 'accessed' => $this->getAtime(), + 'attributes' => $this->getAttributes(), + 'encrypted' => $this->isEncrypted(), + 'encryption_method' => $this->getEncryptionMethod(), + 'comment' => $this->getComment(), + 'crc' => $this->getCrc(), + 'method' => $this->getMethodName(), // deprecated + 'method_name' => $this->getMethodName(), + 'compression_method' => $this->getCompressionMethod(), + 'platform' => $this->getPlatform(), + 'version' => $this->getVersion() + ]; } /** * @return string */ - function __toString() + public function __toString() { - return 'ZipInfo {' - . 'Path="' . $this->getPath() . '", ' - . ($this->isFolder() ? 'Folder, ' : '') - . 'Size=' . FilesUtil::humanSize($this->getSize()) - . ', Compressed size=' . FilesUtil::humanSize($this->getCompressedSize()) - . ', Modified time=' . date(DATE_W3C, $this->getMtime()) . ', ' - . ($this->getCtime() !== null ? 'Created time=' . date(DATE_W3C, $this->getCtime()) . ', ' : '') - . ($this->getAtime() !== null ? 'Accessed time=' . date(DATE_W3C, $this->getAtime()) . ', ' : '') - . ($this->isEncrypted() ? 'Encrypted, ' : '') - . (!empty($this->comment) ? 'Comment="' . $this->getComment() . '", ' : '') - . (!empty($this->crc) ? 'Crc=0x' . dechex($this->getCrc()) . ', ' : '') - . 'Method="' . $this->getMethod() . '", ' - . 'Attributes="' . $this->getAttributes() . '", ' - . 'Platform="' . $this->getPlatform() . '", ' - . 'Version=' . $this->getVersion() - . '}'; + return __CLASS__ . ' {' + . 'Name="' . $this->getName() . '", ' + . ($this->isFolder() ? 'Folder, ' : '') + . 'Size="' . FilesUtil::humanSize($this->getSize()) . '"' + . ', Compressed size="' . FilesUtil::humanSize($this->getCompressedSize()) . '"' + . ', Modified time="' . date(DATE_W3C, $this->getMtime()) . '", ' + . ($this->getCtime() !== null ? 'Created time="' . date(DATE_W3C, $this->getCtime()) . '", ' : '') + . ($this->getAtime() !== null ? 'Accessed time="' . date(DATE_W3C, $this->getAtime()) . '", ' : '') + . ($this->isEncrypted() ? 'Encrypted, ' : '') + . (!empty($this->comment) ? 'Comment="' . $this->getComment() . '", ' : '') + . (!empty($this->crc) ? 'Crc=0x' . dechex($this->getCrc()) . ', ' : '') + . 'Method name="' . $this->getMethodName() . '", ' + . 'Attributes="' . $this->getAttributes() . '", ' + . 'Platform="' . $this->getPlatform() . '", ' + . 'Version=' . $this->getVersion() + . '}'; } - - -} \ No newline at end of file +} diff --git a/src/PhpZip/Model/ZipModel.php b/src/PhpZip/Model/ZipModel.php new file mode 100644 index 0000000..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 @@ class DateTimeConverter $date['mday'] << 16 | $date['hours'] << 11 | $date['minutes'] << 5 | $date['seconds'] >> 1); } -} \ No newline at end of file +} diff --git a/src/PhpZip/Util/FilesUtil.php b/src/PhpZip/Util/FilesUtil.php index 9192c6d..29e8688 100644 --- a/src/PhpZip/Util/FilesUtil.php +++ b/src/PhpZip/Util/FilesUtil.php @@ -1,4 +1,5 @@ 0 && !$escaping) { $regexPattern .= ')'; $inCurrent--; - } else if ($escaping) + } elseif ($escaping) { $regexPattern = "\\}"; - else + } else { $regexPattern = "}"; + } $escaping = false; break; case ',': if ($inCurrent > 0 && !$escaping) { $regexPattern .= '|'; - } else if ($escaping) + } elseif ($escaping) { $regexPattern .= "\\,"; - else + } else { $regexPattern = ","; + } break; default: $escaping = false; @@ -211,12 +214,15 @@ class FilesUtil */ public static function humanSize($size, $unit = null) { - if (($unit === null && $size >= 1 << 30) || $unit === "GB") + if (($unit === null && $size >= 1 << 30) || $unit === "GB") { return number_format($size / (1 << 30), 2) . "GB"; - if (($unit === null && $size >= 1 << 20) || $unit === "MB") + } + if (($unit === null && $size >= 1 << 20) || $unit === "MB") { return number_format($size / (1 << 20), 2) . "MB"; - if (($unit === null && $size >= 1 << 10) || $unit === "KB") + } + if (($unit === null && $size >= 1 << 10) || $unit === "KB") { return number_format($size / (1 << 10), 2) . "KB"; + } return number_format($size) . " bytes"; } -} \ No newline at end of file +} diff --git a/src/PhpZip/Util/Iterator/IgnoreFilesFilterIterator.php b/src/PhpZip/Util/Iterator/IgnoreFilesFilterIterator.php index a3b9c9d..40e8fe0 100644 --- a/src/PhpZip/Util/Iterator/IgnoreFilesFilterIterator.php +++ b/src/PhpZip/Util/Iterator/IgnoreFilesFilterIterator.php @@ -1,4 +1,5 @@ ignoreFiles as $ignoreFile) { // handler dir and sub dir if ($fileInfo->isDir() - && $ignoreFile[strlen($ignoreFile) - 1] === '/' + && StringUtil::endsWith($ignoreFile, '/') && StringUtil::endsWith($pathname, substr($ignoreFile, 0, -1)) ) { return false; @@ -57,4 +58,4 @@ class IgnoreFilesFilterIterator extends \FilterIterator } return true; } -} \ No newline at end of file +} diff --git a/src/PhpZip/Util/Iterator/IgnoreFilesRecursiveFilterIterator.php b/src/PhpZip/Util/Iterator/IgnoreFilesRecursiveFilterIterator.php index 131ee3f..7781576 100644 --- a/src/PhpZip/Util/Iterator/IgnoreFilesRecursiveFilterIterator.php +++ b/src/PhpZip/Util/Iterator/IgnoreFilesRecursiveFilterIterator.php @@ -1,4 +1,5 @@ getInnerIterator()->getChildren(), $this->ignoreFiles); } -} \ No newline at end of file +} diff --git a/src/PhpZip/Util/PackUtil.php b/src/PhpZip/Util/PackUtil.php index 1ef10d0..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 @@ class PackUtil */ 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 @@ class ZipFile implements \Countable, \ArrayAccess, \Iterator * Open zip archive from raw string data. * * @param string $data - * @return ZipFile + * @return ZipFileInterface * @throws InvalidArgumentException if data not available. * @throws ZipException if can't open temp stream. */ @@ -151,7 +133,7 @@ class ZipFile implements \Countable, \ArrayAccess, \Iterator * Open zip archive from stream resource * * @param resource $handle - * @return ZipFile + * @return ZipFileInterface * @throws InvalidArgumentException Invalid stream resource * or resource cannot seekable stream */ @@ -160,44 +142,36 @@ class ZipFile implements \Countable, \ArrayAccess, \Iterator if (!is_resource($handle)) { throw new InvalidArgumentException("Invalid stream resource."); } + $type = get_resource_type($handle); + if ('stream' !== $type) { + throw new InvalidArgumentException("Invalid resource type - $type."); + } $meta = stream_get_meta_data($handle); + if ('dir' === $meta['stream_type']) { + throw new InvalidArgumentException("Invalid stream type - {$meta['stream_type']}."); + } if (!$meta['seekable']) { throw new InvalidArgumentException("Resource cannot seekable stream."); } - $this->inputStream = $handle; - $this->centralDirectory = new CentralDirectory(); - $this->centralDirectory->mountCentralDirectory($this->inputStream); + $this->inputStream = new ZipInputStream($handle); + $this->zipModel = $this->inputStream->readZip(); return $this; } - /** - * @return int Returns the number of entries in this ZIP file. - */ - public function count() - { - return sizeof($this->centralDirectory->getEntries()); - } - /** * @return string[] Returns the list files. */ public function getListFiles() { - return array_keys($this->centralDirectory->getEntries()); + return array_keys($this->zipModel->getEntries()); } /** - * Check whether the directory entry. - * Returns true if and only if this ZIP entry represents a directory entry - * (i.e. end with '/'). - * - * @param string $entryName - * @return bool - * @throws ZipNotFoundEntry + * @return int Returns the number of entries in this ZIP file. */ - public function isDirectory($entryName) + public function count() { - return $this->centralDirectory->getEntry($entryName)->isDirectory(); + return $this->zipModel->count(); } /** @@ -207,34 +181,34 @@ class ZipFile implements \Countable, \ArrayAccess, \Iterator */ public function getArchiveComment() { - return $this->centralDirectory->getArchiveComment(); - } - - /** - * Set password to all input encrypted entries. - * - * @param string $password Password - * @return ZipFile - */ - public function withReadPassword($password) - { - foreach ($this->centralDirectory->getEntries() as $entry) { - if ($entry->isEncrypted()) { - $entry->setPassword($password); - } - } - return $this; + return $this->zipModel->getArchiveComment(); } /** * Set archive comment. * * @param null|string $comment + * @return ZipFileInterface * @throws InvalidArgumentException Length comment out of range */ public function setArchiveComment($comment = null) { - $this->centralDirectory->getEndOfCentralDirectory()->setComment($comment); + $this->zipModel->setArchiveComment($comment); + return $this; + } + + /** + * Checks that the entry in the archive is a directory. + * Returns true if and only if this ZIP entry represents a directory entry + * (i.e. end with '/'). + * + * @param string $entryName + * @return bool + * @throws ZipNotFoundEntry + */ + public function isDirectory($entryName) + { + return $this->zipModel->getEntry($entryName)->isDirectory(); } /** @@ -246,7 +220,7 @@ class ZipFile implements \Countable, \ArrayAccess, \Iterator */ public function getEntryComment($entryName) { - return $this->centralDirectory->getEntry($entryName)->getComment(); + return $this->zipModel->getEntry($entryName)->getComment(); } /** @@ -254,15 +228,37 @@ class ZipFile implements \Countable, \ArrayAccess, \Iterator * * @param string $entryName * @param string|null $comment - * @return ZipFile + * @return ZipFileInterface * @throws ZipNotFoundEntry */ public function setEntryComment($entryName, $comment = null) { - $this->centralDirectory->setEntryComment($entryName, $comment); + $this->zipModel->getEntryForChanges($entryName)->setComment($comment); return $this; } + /** + * Returns the entry contents. + * + * @param string $entryName + * @return string + */ + public function getEntryContents($entryName) + { + return $this->zipModel->getEntry($entryName)->getEntryContent(); + } + + /** + * Checks if there is an entry in the archive. + * + * @param string $entryName + * @return bool + */ + public function hasEntry($entryName) + { + return $this->zipModel->hasEntry($entryName); + } + /** * Get info by entry. * @@ -272,10 +268,7 @@ class ZipFile implements \Countable, \ArrayAccess, \Iterator */ public function getEntryInfo($entryName) { - if (!($entryName instanceof ZipEntry)) { - $entryName = $this->centralDirectory->getEntry($entryName); - } - return new ZipInfo($entryName); + return new ZipInfo($this->zipModel->getEntry($entryName)); } /** @@ -285,7 +278,15 @@ class ZipFile implements \Countable, \ArrayAccess, \Iterator */ public function getAllInfo() { - return array_map([$this, 'getEntryInfo'], $this->centralDirectory->getEntries()); + return array_map([$this, 'getEntryInfo'], $this->zipModel->getEntries()); + } + + /** + * @return ZipEntryMatcher + */ + public function matcher() + { + return $this->zipModel->matcher(); } /** @@ -296,7 +297,7 @@ class ZipFile implements \Countable, \ArrayAccess, \Iterator * @param string $destination Location where to extract the files. * @param array|string|null $entries The entries to extract. It accepts either * a single entry name or an array of names. - * @return ZipFile + * @return ZipFileInterface * @throws ZipException */ public function extractTo($destination, $entries = null) @@ -311,9 +312,8 @@ class ZipFile implements \Countable, \ArrayAccess, \Iterator throw new ZipException("Destination is not writable directory"); } - /** - * @var ZipEntry[] $zipEntries - */ + $zipEntries = $this->zipModel->getEntries(); + if (!empty($entries)) { if (is_string($entries)) { $entries = (array)$entries; @@ -321,18 +321,10 @@ class ZipFile implements \Countable, \ArrayAccess, \Iterator if (is_array($entries)) { $entries = array_unique($entries); $flipEntries = array_flip($entries); - $zipEntries = array_filter( - $this->centralDirectory->getEntries(), - function ($zipEntry) use ($flipEntries) { - /** - * @var ZipEntry $zipEntry - */ - return isset($flipEntries[$zipEntry->getName()]); - } - ); + $zipEntries = array_filter($zipEntries, function (ZipEntry $zipEntry) use ($flipEntries) { + return isset($flipEntries[$zipEntry->getName()]); + }); } - } else { - $zipEntries = $this->centralDirectory->getEntries(); } foreach ($zipEntries as $entry) { @@ -355,7 +347,7 @@ class ZipFile implements \Countable, \ArrayAccess, \Iterator chmod($dir, 0755); touch($dir, $entry->getTime()); } - if (file_put_contents($file, $entry->getEntryContent()) === false) { + if (false === file_put_contents($file, $entry->getEntryContent())) { throw new ZipException('Can not extract file ' . $entry->getName()); } touch($file, $entry->getTime()); @@ -371,12 +363,12 @@ class ZipFile implements \Countable, \ArrayAccess, \Iterator * @param int|null $compressionMethod Compression method. * Use ZipFile::METHOD_STORED, ZipFile::METHOD_DEFLATED or ZipFile::METHOD_BZIP2. * If null, then auto choosing method. - * @return ZipFile + * @return ZipFileInterface * @throws InvalidArgumentException If incorrect data or entry name. * @throws ZipUnsupportMethod - * @see ZipFile::METHOD_STORED - * @see ZipFile::METHOD_DEFLATED - * @see ZipFile::METHOD_BZIP2 + * @see ZipFileInterface::METHOD_STORED + * @see ZipFileInterface::METHOD_DEFLATED + * @see ZipFileInterface::METHOD_BZIP2 */ public function addFromString($localName, $contents, $compressionMethod = null) { @@ -390,23 +382,23 @@ class ZipFile implements \Countable, \ArrayAccess, \Iterator $contents = (string)$contents; $length = strlen($contents); if (null === $compressionMethod) { - if ($length >= 1024) { - $compressionMethod = self::METHOD_DEFLATED; + if ($length >= 512) { + $compressionMethod = ZipEntry::UNKNOWN; } else { - $compressionMethod = self::METHOD_STORED; + $compressionMethod = ZipFileInterface::METHOD_STORED; } } elseif (!in_array($compressionMethod, self::$allowCompressionMethods, true)) { - throw new ZipUnsupportMethod('Unsupported method ' . $compressionMethod); + throw new ZipUnsupportMethod('Unsupported compression method ' . $compressionMethod); } $externalAttributes = 0100644 << 16; - $entry = new ZipNewStringEntry($contents); + $entry = new ZipNewEntry($contents); $entry->setName($localName); $entry->setMethod($compressionMethod); $entry->setTime(time()); $entry->setExternalAttributes($externalAttributes); - $this->centralDirectory->putInModified($localName, $entry); + $this->zipModel->addEntry($entry); return $this; } @@ -418,12 +410,12 @@ class ZipFile implements \Countable, \ArrayAccess, \Iterator * @param int|null $compressionMethod Compression method. * Use ZipFile::METHOD_STORED, ZipFile::METHOD_DEFLATED or ZipFile::METHOD_BZIP2. * If null, then auto choosing method. - * @return ZipFile + * @return ZipFileInterface * @throws InvalidArgumentException * @throws ZipUnsupportMethod - * @see ZipFile::METHOD_STORED - * @see ZipFile::METHOD_DEFLATED - * @see ZipFile::METHOD_BZIP2 + * @see ZipFileInterface::METHOD_STORED + * @see ZipFileInterface::METHOD_DEFLATED + * @see ZipFileInterface::METHOD_BZIP2 */ public function addFile($filename, $localName = null, $compressionMethod = null) { @@ -439,16 +431,16 @@ class ZipFile implements \Countable, \ArrayAccess, \Iterator $mimeType = @mime_content_type($filename); $type = strtok($mimeType, '/'); if ('image' === $type) { - $compressionMethod = self::METHOD_STORED; + $compressionMethod = ZipFileInterface::METHOD_STORED; } elseif ('text' === $type && filesize($filename) < 150) { - $compressionMethod = self::METHOD_STORED; + $compressionMethod = ZipFileInterface::METHOD_STORED; } else { - $compressionMethod = self::METHOD_DEFLATED; + $compressionMethod = ZipEntry::UNKNOWN; } - } elseif (@filesize($filename) >= 1024) { - $compressionMethod = self::METHOD_DEFLATED; + } elseif (@filesize($filename) >= 512) { + $compressionMethod = ZipEntry::UNKNOWN; } else { - $compressionMethod = self::METHOD_STORED; + $compressionMethod = ZipFileInterface::METHOD_STORED; } } elseif (!in_array($compressionMethod, self::$allowCompressionMethods, true)) { throw new ZipUnsupportMethod('Unsupported method ' . $compressionMethod); @@ -461,9 +453,7 @@ class ZipFile implements \Countable, \ArrayAccess, \Iterator $localName = basename($filename); } $this->addFromStream($handle, $localName, $compressionMethod); - $this->centralDirectory - ->getModifiedEntry($localName) - ->setTime(filemtime($filename)); + $this->zipModel->getEntry($localName)->setTime(filemtime($filename)); return $this; } @@ -475,12 +465,12 @@ class ZipFile implements \Countable, \ArrayAccess, \Iterator * @param int|null $compressionMethod Compression method. * Use ZipFile::METHOD_STORED, ZipFile::METHOD_DEFLATED or ZipFile::METHOD_BZIP2. * If null, then auto choosing method. - * @return ZipFile + * @return ZipFileInterface * @throws InvalidArgumentException * @throws ZipUnsupportMethod - * @see ZipFile::METHOD_STORED - * @see ZipFile::METHOD_DEFLATED - * @see ZipFile::METHOD_BZIP2 + * @see ZipFileInterface::METHOD_STORED + * @see ZipFileInterface::METHOD_DEFLATED + * @see ZipFileInterface::METHOD_BZIP2 */ public function addFromStream($stream, $localName, $compressionMethod = null) { @@ -494,10 +484,10 @@ class ZipFile implements \Countable, \ArrayAccess, \Iterator $fstat = fstat($stream); $length = $fstat['size']; if (null === $compressionMethod) { - if ($length >= 1024) { - $compressionMethod = self::METHOD_DEFLATED; + if ($length >= 512) { + $compressionMethod = ZipEntry::UNKNOWN; } else { - $compressionMethod = self::METHOD_STORED; + $compressionMethod = ZipFileInterface::METHOD_STORED; } } elseif (!in_array($compressionMethod, self::$allowCompressionMethods, true)) { throw new ZipUnsupportMethod('Unsupported method ' . $compressionMethod); @@ -506,13 +496,13 @@ class ZipFile implements \Countable, \ArrayAccess, \Iterator $mode = sprintf('%o', $fstat['mode']); $externalAttributes = (octdec($mode) & 0xffff) << 16; - $entry = new ZipNewStreamEntry($stream); + $entry = new ZipNewEntry($stream); $entry->setName($localName); $entry->setMethod($compressionMethod); $entry->setTime(time()); $entry->setExternalAttributes($externalAttributes); - $this->centralDirectory->putInModified($localName, $entry); + $this->zipModel->addEntry($entry); return $this; } @@ -520,7 +510,7 @@ class ZipFile implements \Countable, \ArrayAccess, \Iterator * Add an empty directory in the zip archive. * * @param string $dirName - * @return ZipFile + * @return ZipFileInterface * @throws InvalidArgumentException */ public function addEmptyDir($dirName) @@ -532,33 +522,19 @@ class ZipFile implements \Countable, \ArrayAccess, \Iterator $dirName = rtrim($dirName, '/') . '/'; $externalAttributes = 040755 << 16; - $entry = new ZipNewEmptyDirEntry(); + $entry = new ZipNewEntry(); $entry->setName($dirName); $entry->setTime(time()); - $entry->setMethod(self::METHOD_STORED); + $entry->setMethod(ZipFileInterface::METHOD_STORED); $entry->setSize(0); $entry->setCompressedSize(0); $entry->setCrc(0); $entry->setExternalAttributes($externalAttributes); - $this->centralDirectory->putInModified($dirName, $entry); + $this->zipModel->addEntry($entry); return $this; } - /** - * Add array data to archive. - * Keys is local names. - * Values is contents. - * - * @param array $mapData Associative array for added to zip. - */ - public function addAll(array $mapData) - { - foreach ($mapData as $localName => $content) { - $this[$localName] = $content; - } - } - /** * Add directory not recursively to the zip archive. * @@ -567,7 +543,7 @@ class ZipFile implements \Countable, \ArrayAccess, \Iterator * @param int|null $compressionMethod Compression method. * Use ZipFile::METHOD_STORED, ZipFile::METHOD_DEFLATED or ZipFile::METHOD_BZIP2. * If null, then auto choosing method. - * @return ZipFile + * @return ZipFileInterface * @throws InvalidArgumentException */ public function addDir($inputDir, $localPath = "/", $compressionMethod = null) @@ -593,12 +569,12 @@ class ZipFile implements \Countable, \ArrayAccess, \Iterator * @param int|null $compressionMethod Compression method. * Use ZipFile::METHOD_STORED, ZipFile::METHOD_DEFLATED or ZipFile::METHOD_BZIP2. * If null, then auto choosing method. - * @return ZipFile + * @return ZipFileInterface * @throws InvalidArgumentException * @throws ZipUnsupportMethod - * @see ZipFile::METHOD_STORED - * @see ZipFile::METHOD_DEFLATED - * @see ZipFile::METHOD_BZIP2 + * @see ZipFileInterface::METHOD_STORED + * @see ZipFileInterface::METHOD_DEFLATED + * @see ZipFileInterface::METHOD_BZIP2 */ public function addDirRecursive($inputDir, $localPath = "/", $compressionMethod = null) { @@ -623,19 +599,18 @@ class ZipFile implements \Countable, \ArrayAccess, \Iterator * @param int|null $compressionMethod Compression method. * Use ZipFile::METHOD_STORED, ZipFile::METHOD_DEFLATED or ZipFile::METHOD_BZIP2. * If null, then auto choosing method. - * @return ZipFile + * @return ZipFileInterface * @throws InvalidArgumentException * @throws ZipUnsupportMethod - * @see ZipFile::METHOD_STORED - * @see ZipFile::METHOD_DEFLATED - * @see ZipFile::METHOD_BZIP2 + * @see ZipFileInterface::METHOD_STORED + * @see ZipFileInterface::METHOD_DEFLATED + * @see ZipFileInterface::METHOD_BZIP2 */ public function addFilesFromIterator( \Iterator $iterator, $localPath = '/', $compressionMethod = null - ) - { + ) { $localPath = (string)$localPath; if (null !== $localPath && 0 !== strlen($localPath)) { $localPath = rtrim($localPath, '/'); @@ -690,7 +665,7 @@ class ZipFile implements \Countable, \ArrayAccess, \Iterator * @param int|null $compressionMethod Compression method. * Use ZipFile::METHOD_STORED, ZipFile::METHOD_DEFLATED or ZipFile::METHOD_BZIP2. * If null, then auto choosing method. - * @return ZipFile + * @return ZipFileInterface * @throws InvalidArgumentException * @sse https://en.wikipedia.org/wiki/Glob_(programming) Glob pattern syntax */ @@ -699,24 +674,6 @@ class ZipFile implements \Countable, \ArrayAccess, \Iterator return $this->addGlob($inputDir, $globPattern, $localPath, false, $compressionMethod); } - /** - * Add files recursively from glob pattern. - * - * @param string $inputDir Input directory - * @param string $globPattern Glob pattern. - * @param string|null $localPath Add files to this directory, or the root. - * @param int|null $compressionMethod Compression method. - * Use ZipFile::METHOD_STORED, ZipFile::METHOD_DEFLATED or ZipFile::METHOD_BZIP2. - * If null, then auto choosing method. - * @return ZipFile - * @throws InvalidArgumentException - * @sse https://en.wikipedia.org/wiki/Glob_(programming) Glob pattern syntax - */ - public function addFilesFromGlobRecursive($inputDir, $globPattern, $localPath = '/', $compressionMethod = null) - { - return $this->addGlob($inputDir, $globPattern, $localPath, true, $compressionMethod); - } - /** * Add files from glob pattern. * @@ -727,7 +684,7 @@ class ZipFile implements \Countable, \ArrayAccess, \Iterator * @param int|null $compressionMethod Compression method. * Use ZipFile::METHOD_STORED, ZipFile::METHOD_DEFLATED or ZipFile::METHOD_BZIP2. * If null, then auto choosing method. - * @return ZipFile + * @return ZipFileInterface * @throws InvalidArgumentException * @sse https://en.wikipedia.org/wiki/Glob_(programming) Glob pattern syntax */ @@ -737,8 +694,7 @@ class ZipFile implements \Countable, \ArrayAccess, \Iterator $localPath = '/', $recursive = true, $compressionMethod = null - ) - { + ) { $inputDir = (string)$inputDir; if (null === $inputDir || 0 === strlen($inputDir)) { throw new InvalidArgumentException('Input dir empty'); @@ -779,6 +735,24 @@ class ZipFile implements \Countable, \ArrayAccess, \Iterator return $this; } + /** + * Add files recursively from glob pattern. + * + * @param string $inputDir Input directory + * @param string $globPattern Glob pattern. + * @param string|null $localPath Add files to this directory, or the root. + * @param int|null $compressionMethod Compression method. + * Use ZipFile::METHOD_STORED, ZipFile::METHOD_DEFLATED or ZipFile::METHOD_BZIP2. + * If null, then auto choosing method. + * @return ZipFileInterface + * @throws InvalidArgumentException + * @sse https://en.wikipedia.org/wiki/Glob_(programming) Glob pattern syntax + */ + public function addFilesFromGlobRecursive($inputDir, $globPattern, $localPath = '/', $compressionMethod = null) + { + return $this->addGlob($inputDir, $globPattern, $localPath, true, $compressionMethod); + } + /** * Add files from regex pattern. * @@ -788,7 +762,7 @@ class ZipFile implements \Countable, \ArrayAccess, \Iterator * @param int|null $compressionMethod Compression method. * Use ZipFile::METHOD_STORED, ZipFile::METHOD_DEFLATED or ZipFile::METHOD_BZIP2. * If null, then auto choosing method. - * @return ZipFile + * @return ZipFileInterface * @internal param bool $recursive Recursive search. */ public function addFilesFromRegex($inputDir, $regexPattern, $localPath = "/", $compressionMethod = null) @@ -796,24 +770,6 @@ class ZipFile implements \Countable, \ArrayAccess, \Iterator return $this->addRegex($inputDir, $regexPattern, $localPath, false, $compressionMethod); } - /** - * Add files recursively from regex pattern. - * - * @param string $inputDir Search files in this directory. - * @param string $regexPattern Regex pattern. - * @param string|null $localPath Add files to this directory, or the root. - * @param int|null $compressionMethod Compression method. - * Use ZipFile::METHOD_STORED, ZipFile::METHOD_DEFLATED or ZipFile::METHOD_BZIP2. - * If null, then auto choosing method. - * @return ZipFile - * @internal param bool $recursive Recursive search. - */ - public function addFilesFromRegexRecursive($inputDir, $regexPattern, $localPath = "/", $compressionMethod = null) - { - return $this->addRegex($inputDir, $regexPattern, $localPath, true, $compressionMethod); - } - - /** * Add files from regex pattern. * @@ -824,7 +780,7 @@ class ZipFile implements \Countable, \ArrayAccess, \Iterator * @param int|null $compressionMethod Compression method. * Use ZipFile::METHOD_STORED, ZipFile::METHOD_DEFLATED or ZipFile::METHOD_BZIP2. * If null, then auto choosing method. - * @return ZipFile + * @return ZipFileInterface * @throws InvalidArgumentException */ private function addRegex( @@ -833,8 +789,7 @@ class ZipFile implements \Countable, \ArrayAccess, \Iterator $localPath = "/", $recursive = true, $compressionMethod = null - ) - { + ) { $regexPattern = (string)$regexPattern; if (empty($regexPattern)) { throw new InvalidArgumentException("regex pattern empty"); @@ -874,12 +829,43 @@ class ZipFile implements \Countable, \ArrayAccess, \Iterator return $this; } + /** + * Add files recursively from regex pattern. + * + * @param string $inputDir Search files in this directory. + * @param string $regexPattern Regex pattern. + * @param string|null $localPath Add files to this directory, or the root. + * @param int|null $compressionMethod Compression method. + * Use ZipFile::METHOD_STORED, ZipFile::METHOD_DEFLATED or ZipFile::METHOD_BZIP2. + * If null, then auto choosing method. + * @return ZipFileInterface + * @internal param bool $recursive Recursive search. + */ + public function addFilesFromRegexRecursive($inputDir, $regexPattern, $localPath = "/", $compressionMethod = null) + { + return $this->addRegex($inputDir, $regexPattern, $localPath, true, $compressionMethod); + } + + /** + * Add array data to archive. + * Keys is local names. + * Values is contents. + * + * @param array $mapData Associative array for added to zip. + */ + public function addAll(array $mapData) + { + foreach ($mapData as $localName => $content) { + $this[$localName] = $content; + } + } + /** * Rename the entry. * * @param string $oldName Old entry name. * @param string $newName New entry name. - * @return ZipFile + * @return ZipFileInterface * @throws InvalidArgumentException * @throws ZipNotFoundEntry */ @@ -888,7 +874,9 @@ class ZipFile implements \Countable, \ArrayAccess, \Iterator if (null === $oldName || null === $newName) { throw new InvalidArgumentException("name is null"); } - $this->centralDirectory->rename($oldName, $newName); + if ($oldName !== $newName) { + $this->zipModel->renameEntry($oldName, $newName); + } return $this; } @@ -896,13 +884,15 @@ class ZipFile implements \Countable, \ArrayAccess, \Iterator * Delete entry by name. * * @param string $entryName Zip Entry name. - * @return ZipFile + * @return ZipFileInterface * @throws ZipNotFoundEntry If entry not found. */ public function deleteFromName($entryName) { $entryName = (string)$entryName; - $this->centralDirectory->deleteEntry($entryName); + if (!$this->zipModel->deleteEntry($entryName)) { + throw new ZipNotFoundEntry("Entry " . $entryName . ' not found!'); + } return $this; } @@ -910,7 +900,7 @@ class ZipFile implements \Countable, \ArrayAccess, \Iterator * Delete entries by glob pattern. * * @param string $globPattern Glob pattern - * @return ZipFile + * @return ZipFileInterface * @throws InvalidArgumentException * @sse https://en.wikipedia.org/wiki/Glob_(programming) Glob pattern syntax */ @@ -928,7 +918,7 @@ class ZipFile implements \Countable, \ArrayAccess, \Iterator * Delete entries by regex pattern. * * @param string $regexPattern Regex pattern - * @return ZipFile + * @return ZipFileInterface * @throws InvalidArgumentException */ public function deleteFromRegex($regexPattern) @@ -936,17 +926,17 @@ class ZipFile implements \Countable, \ArrayAccess, \Iterator if (null === $regexPattern || !is_string($regexPattern) || empty($regexPattern)) { throw new InvalidArgumentException("Regex pattern is empty."); } - $this->centralDirectory->deleteEntriesFromRegex($regexPattern); + $this->matcher()->match($regexPattern)->delete(); return $this; } /** * Delete all entries - * @return ZipFile + * @return ZipFileInterface */ public function deleteAll() { - $this->centralDirectory->deleteAll(); + $this->zipModel->deleteAll(); return $this; } @@ -954,43 +944,241 @@ class ZipFile implements \Countable, \ArrayAccess, \Iterator * Set compression level for new entries. * * @param int $compressionLevel - * @see ZipFile::LEVEL_DEFAULT_COMPRESSION - * @see ZipFile::LEVEL_BEST_SPEED - * @see ZipFile::LEVEL_BEST_COMPRESSION + * @return ZipFileInterface + * @throws InvalidArgumentException + * @see ZipFileInterface::LEVEL_DEFAULT_COMPRESSION + * @see ZipFileInterface::LEVEL_SUPER_FAST + * @see ZipFileInterface::LEVEL_FAST + * @see ZipFileInterface::LEVEL_BEST_COMPRESSION */ - public function setCompressionLevel($compressionLevel = self::LEVEL_DEFAULT_COMPRESSION) + public function setCompressionLevel($compressionLevel = ZipFileInterface::LEVEL_DEFAULT_COMPRESSION) { - $this->centralDirectory->setCompressionLevel($compressionLevel); + if ($compressionLevel < ZipFileInterface::LEVEL_DEFAULT_COMPRESSION || + $compressionLevel > ZipFileInterface::LEVEL_BEST_COMPRESSION + ) { + throw new InvalidArgumentException('Invalid compression level. Minimum level ' . + ZipFileInterface::LEVEL_DEFAULT_COMPRESSION . '. Maximum level ' . ZipFileInterface::LEVEL_BEST_COMPRESSION); + } + $this->matcher()->all()->invoke(function ($entry) use ($compressionLevel) { + $this->setCompressionLevelEntry($entry, $compressionLevel); + }); + return $this; } /** + * @param string $entryName + * @param int $compressionLevel + * @return ZipFileInterface + * @throws ZipException + * @see ZipFileInterface::LEVEL_DEFAULT_COMPRESSION + * @see ZipFileInterface::LEVEL_SUPER_FAST + * @see ZipFileInterface::LEVEL_FAST + * @see ZipFileInterface::LEVEL_BEST_COMPRESSION + */ + public function setCompressionLevelEntry($entryName, $compressionLevel) + { + if (null !== $compressionLevel) { + if ($compressionLevel < ZipFileInterface::LEVEL_DEFAULT_COMPRESSION || + $compressionLevel > ZipFileInterface::LEVEL_BEST_COMPRESSION + ) { + throw new InvalidArgumentException('Invalid compression level. Minimum level ' . + ZipFileInterface::LEVEL_DEFAULT_COMPRESSION . '. Maximum level ' . ZipFileInterface::LEVEL_BEST_COMPRESSION); + } + $entry = $this->zipModel->getEntry($entryName); + if ($entry->getCompressionLevel() !== $compressionLevel) { + $entry = $this->zipModel->getEntryForChanges($entry); + $entry->setCompressionLevel($compressionLevel); + } + } + return $this; + } + + /** + * @param string $entryName + * @param int $compressionMethod + * @return ZipFileInterface + * @throws ZipException + * @see ZipFileInterface::METHOD_STORED + * @see ZipFileInterface::METHOD_DEFLATED + * @see ZipFileInterface::METHOD_BZIP2 + */ + public function setCompressionMethodEntry($entryName, $compressionMethod) + { + if (!in_array($compressionMethod, self::$allowCompressionMethods, true)) { + throw new ZipUnsupportMethod('Unsupported method ' . $compressionMethod); + } + $entry = $this->zipModel->getEntry($entryName); + if ($entry->getMethod() !== $compressionMethod) { + $this->zipModel + ->getEntryForChanges($entry) + ->setMethod($compressionMethod); + } + return $this; + } + + /** + * zipalign is optimization to Android application (APK) files. + * * @param int|null $align + * @return ZipFileInterface + * @link https://developer.android.com/studio/command-line/zipalign.html */ public function setZipAlign($align = null) { - $this->centralDirectory->setZipAlign($align); + $this->zipModel->setZipAlign($align); + return $this; + } + + /** + * Set password to all input encrypted entries. + * + * @param string $password Password + * @return ZipFileInterface + * @deprecated using ZipFileInterface::setReadPassword() + */ + public function withReadPassword($password) + { + return $this->setReadPassword($password); + } + + /** + * Set password to all input encrypted entries. + * + * @param string $password Password + * @return ZipFileInterface + */ + public function setReadPassword($password) + { + $this->zipModel->setReadPassword($password); + return $this; + } + + /** + * Set password to concrete input entry. + * + * @param string $entryName + * @param string $password Password + * @return ZipFileInterface + */ + public function setReadPasswordEntry($entryName, $password) + { + $this->zipModel->setReadPasswordEntry($entryName, $password); + return $this; } /** * Set password for all entries for update. * * @param string $password If password null then encryption clear - * @param int $encryptionMethod Encryption method - * @return ZipFile + * @param int|null $encryptionMethod Encryption method + * @return ZipFileInterface + * @deprecated using ZipFileInterface::setPassword() */ - public function withNewPassword($password, $encryptionMethod = self::ENCRYPTION_METHOD_WINZIP_AES) + public function withNewPassword($password, $encryptionMethod = self::ENCRYPTION_METHOD_WINZIP_AES_256) { - $this->centralDirectory->setNewPassword($password, $encryptionMethod); + return $this->setPassword($password, $encryptionMethod); + } + + /** + * 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 @@ class ZipFile implements \Countable, \ArrayAccess, \Iterator * Save as file. * * @param string $filename Output filename + * @return ZipFileInterface * @throws InvalidArgumentException * @throws ZipException */ @@ -1014,12 +1203,14 @@ class ZipFile implements \Countable, \ArrayAccess, \Iterator if (!@rename($tempFilename, $filename)) { throw new ZipException('Can not move ' . $tempFilename . ' to ' . $filename); } + return $this; } /** * Save as stream. * * @param resource $handle Output stream resource + * @return ZipFileInterface * @throws ZipException */ public function saveAsStream($handle) @@ -1028,44 +1219,102 @@ class ZipFile implements \Countable, \ArrayAccess, \Iterator throw new InvalidArgumentException('handle is not resource'); } ftruncate($handle, 0); - $this->centralDirectory->writeArchive($handle); + $this->writeZipToStream($handle); fclose($handle); + return $this; } /** * 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,85 +1325,62 @@ class ZipFile implements \Countable, \ArrayAccess, \Iterator if (!($handle = fopen('php://memory', 'w+b'))) { throw new InvalidArgumentException("Memory can not open from write."); } - $this->centralDirectory->writeArchive($handle); + $this->writeZipToStream($handle); rewind($handle); $content = stream_get_contents($handle); fclose($handle); return $content; } - /** - * Rewrite and reopen zip archive. - * @return ZipFile - * @throws ZipException - */ - public function rewrite() - { - if (null === $this->inputStream) { - throw new ZipException('input stream is null'); - } - $meta = stream_get_meta_data($this->inputStream); - $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."); - } - if (!($handle = @fopen($meta['uri'], 'rb'))) { - throw new ZipException("File {$meta['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->close(); $this->inputStream = null; + $this->zipModel = new ZipModel(); } - if (null !== $this->centralDirectory) { - $this->centralDirectory->release(); - $this->centralDirectory = null; + } + + /** + * Save and reopen zip archive. + * @return ZipFileInterface + * @throws ZipException + */ + public function rewrite() + { + if (null === $this->inputStream) { + throw new ZipException('input stream is null'); } + $meta = stream_get_meta_data($this->inputStream->getStream()); + $content = $this->outputAsString(); + $this->close(); + if ('plainfile' === $meta['wrapper_type']) { + /** + * @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($uri, 'rb'))) { + throw new ZipException("File {$uri} can't open."); + } + return $this->openFromStream($handle); + } + return $this->openFromString($content); } /** * 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 @@ class ZipFile implements \Countable, \ArrayAccess, \Iterator $this->addFile($contents->getPathname(), $entryName); return; } - $contents = (string)$contents; - if ('/' === $entryName[strlen($entryName) - 1]) { + if (StringUtil::endsWith($entryName, '/')) { $this->addEmptyDir($entryName); + } elseif (is_resource($contents)) { + $this->addFromStream($contents, $entryName); } else { + $contents = (string)$contents; $this->addFromString($entryName, $contents); } } @@ -1214,14 +1442,15 @@ class ZipFile implements \Countable, \ArrayAccess, \Iterator } /** - * Move forward to next element - * @link http://php.net/manual/en/iterator.next.php - * @return void Any returned value is ignored. - * @since 5.0.0 + * Offset to retrieve + * @link http://php.net/manual/en/arrayaccess.offsetget.php + * @param string $entryName The offset to retrieve. + * @return string|null + * @throws ZipNotFoundEntry */ - public function next() + public function offsetGet($entryName) { - next($this->centralDirectory->getEntries()); + return $this->getEntryContents($entryName); } /** @@ -1232,7 +1461,18 @@ class ZipFile implements \Countable, \ArrayAccess, \Iterator */ public function key() { - return key($this->centralDirectory->getEntries()); + return key($this->zipModel->getEntries()); + } + + /** + * Move forward to next element + * @link http://php.net/manual/en/iterator.next.php + * @return void Any returned value is ignored. + * @since 5.0.0 + */ + public function next() + { + next($this->zipModel->getEntries()); } /** @@ -1247,6 +1487,18 @@ class ZipFile implements \Countable, \ArrayAccess, \Iterator return $this->offsetExists($this->key()); } + /** + * Whether a offset exists + * @link http://php.net/manual/en/arrayaccess.offsetexists.php + * @param string $entryName An offset to check for. + * @return boolean true on success or false on failure. + * The return value will be casted to boolean if non-boolean was returned. + */ + public function offsetExists($entryName) + { + return $this->hasEntry($entryName); + } + /** * Rewind the Iterator to the first element * @link http://php.net/manual/en/iterator.rewind.php @@ -1255,6 +1507,6 @@ class ZipFile implements \Countable, \ArrayAccess, \Iterator */ public function rewind() { - reset($this->centralDirectory->getEntries()); + reset($this->zipModel->getEntries()); } -} \ No newline at end of file +} diff --git a/src/PhpZip/ZipFileInterface.php b/src/PhpZip/ZipFileInterface.php new file mode 100644 index 0000000..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 @@ class ZipFileAddDirTest extends ZipTestCase } } - protected static function assertFilesResult(ZipFile $zipFile, array $actualResultFiles = [], $localPath = '/') + protected static function assertFilesResult(ZipFileInterface $zipFile, array $actualResultFiles = [], $localPath = '/') { $localPath = rtrim($localPath, '/'); $localPath = empty($localPath) ? "" : $localPath . '/'; @@ -134,6 +135,29 @@ class ZipFileAddDirTest extends ZipTestCase $zipFile->close(); } + public function testAddFilesFromIteratorEmptyLocalPath() + { + $localPath = ''; + + $directoryIterator = new \DirectoryIterator($this->outputDirname); + + $zipFile = new ZipFile(); + $zipFile->addFilesFromIterator($directoryIterator, $localPath); + $zipFile->saveAsFile($this->outputFilename); + $zipFile->close(); + + self::assertCorrectZipArchive($this->outputFilename); + + $zipFile->openFile($this->outputFilename); + self::assertFilesResult($zipFile, [ + '.hidden', + 'text file.txt', + 'Текстовый документ.txt', + 'empty dir/', + ]); + $zipFile->close(); + } + public function testAddFilesFromRecursiveIterator() { $localPath = 'to/project'; @@ -182,7 +206,8 @@ class ZipFileAddDirTest extends ZipTestCase $zipFile->close(); } - public function testAddFilesFromIteratorWithIgnoreFiles(){ + public function testAddFilesFromIteratorWithIgnoreFiles() + { $localPath = 'to/project'; $ignoreFiles = [ 'Текстовый документ.txt', @@ -207,7 +232,8 @@ class ZipFileAddDirTest extends ZipTestCase $zipFile->close(); } - public function testAddFilesFromRecursiveIteratorWithIgnoreFiles(){ + public function testAddFilesFromRecursiveIteratorWithIgnoreFiles() + { $localPath = 'to/project'; $ignoreFiles = [ '.hidden', @@ -354,6 +380,4 @@ class ZipFileAddDirTest extends ZipTestCase self::assertFilesResult($zipFile, array_keys(self::$files), $localPath); $zipFile->close(); } - - -} \ No newline at end of file +} diff --git a/tests/PhpZip/ZipFileTest.php b/tests/PhpZip/ZipFileTest.php index 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 @@ class ZipFileTest extends ZipTestCase */ public function testOpenFileCantOpen() { + if (0 === posix_getuid()) { + $this->markTestSkipped('Skip the test for a user with root privileges'); + } + self::assertNotFalse(file_put_contents($this->outputFilename, 'content')); self::assertTrue(chmod($this->outputFilename, 0222)); @@ -122,9 +127,33 @@ class ZipFileTest extends ZipTestCase 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 @@ class ZipFileTest extends ZipTestCase $zipFile = new ZipFile(); $zipFile ->addFromString('file', 'content') - ->saveAsFile($this->outputFilename); - $zipFile->close(); + ->saveAsFile($this->outputFilename) + ->close(); $handle = fopen($this->outputFilename, 'rb'); $zipFile->openFromStream($handle); @@ -187,16 +216,18 @@ class ZipFileTest extends ZipTestCase public function testEmptyArchive() { $zipFile = new ZipFile(); - $zipFile->saveAsFile($this->outputFilename); - $zipFile->close(); + $zipFile + ->saveAsFile($this->outputFilename) + ->close(); self::assertCorrectEmptyZip($this->outputFilename); self::assertTrue(mkdir($this->outputDirname, 0755, true)); $zipFile->openFile($this->outputFilename); self::assertEquals($zipFile->count(), 0); - $zipFile->extractTo($this->outputDirname); - $zipFile->close(); + $zipFile + ->extractTo($this->outputDirname) + ->close(); self::assertTrue(FilesUtil::isEmptyDir($this->outputDirname)); } @@ -214,18 +245,23 @@ class ZipFileTest extends ZipTestCase $fileExpected = $this->outputDirname . DIRECTORY_SEPARATOR . 'file_expected.zip'; $zipFile = new ZipFile(); - $zipFile->addDirRecursive(__DIR__); - $zipFile->saveAsFile($fileActual); + $zipFile->addDirRecursive(__DIR__.'/../../src'); + $sourceCount = $zipFile->count(); + self::assertTrue($sourceCount > 0); + $zipFile + ->saveAsFile($fileActual) + ->close(); self::assertCorrectZipArchive($fileActual); - $zipFile->close(); - $zipFile->openFile($fileActual); - $zipFile->saveAsFile($fileExpected); + $zipFile + ->openFile($fileActual) + ->saveAsFile($fileExpected); self::assertCorrectZipArchive($fileExpected); $zipFileExpected = new ZipFile(); $zipFileExpected->openFile($fileExpected); + self::assertEquals($zipFile->count(), $sourceCount); self::assertEquals($zipFileExpected->count(), $zipFile->count()); self::assertEquals($zipFileExpected->getListFiles(), $zipFile->getListFiles()); @@ -243,13 +279,13 @@ class ZipFileTest extends ZipTestCase * @see ZipOutputFile::addFromString() * @see ZipOutputFile::addFromFile() * @see ZipOutputFile::addFromStream() - * @see ZipFile::getEntryContent() + * @see ZipFile::getEntryContents() */ public function testCreateArchiveAndAddFiles() { $outputFromString = file_get_contents(__FILE__); $outputFromString2 = file_get_contents(dirname(dirname(__DIR__)) . DIRECTORY_SEPARATOR . 'README.md'); - $outputFromFile = file_get_contents(dirname(dirname(__DIR__)) . DIRECTORY_SEPARATOR . 'bootstrap.xml'); + $outputFromFile = file_get_contents(dirname(dirname(__DIR__)) . DIRECTORY_SEPARATOR . 'phpunit.xml'); $outputFromStream = file_get_contents(dirname(dirname(__DIR__)) . DIRECTORY_SEPARATOR . 'composer.json'); $filenameFromString = basename(__FILE__); @@ -267,15 +303,18 @@ class ZipFileTest extends ZipTestCase fwrite($tempStream, $outputFromStream); $zipFile = new ZipFile; - $zipFile->addFromString($filenameFromString, $outputFromString); - $zipFile->addFile($tempFile, $filenameFromFile); - $zipFile->addFromStream($tempStream, $filenameFromStream); - $zipFile->addEmptyDir($emptyDirName); + $zipFile + ->addFromString($filenameFromString, $outputFromString) + ->addFile($tempFile, $filenameFromFile) + ->addFromStream($tempStream, $filenameFromStream) + ->addEmptyDir($emptyDirName); $zipFile[$filenameFromString2] = $outputFromString2; $zipFile[$emptyDirName2] = null; $zipFile[$emptyDirName3] = 'this content ignoring'; - $zipFile->saveAsFile($this->outputFilename); - $zipFile->close(); + self::assertEquals(count($zipFile), 7); + $zipFile + ->saveAsFile($this->outputFilename) + ->close(); unlink($tempFile); self::assertCorrectZipArchive($this->outputFilename); @@ -305,6 +344,18 @@ class ZipFileTest extends ZipTestCase $zipFile->close(); } + public function testEmptyContent() + { + $zipFile = new ZipFile(); + $zipFile['file'] = ''; + $zipFile->saveAsFile($this->outputFilename); + $zipFile->close(); + + $zipFile->openFile($this->outputFilename); + self::assertEquals($zipFile['file'], ''); + $zipFile->close(); + } + /** * Test compression method from image file. */ @@ -331,7 +382,7 @@ class ZipFileTest extends ZipTestCase $zipFile->openFile($this->outputFilename); $info = $zipFile->getEntryInfo($basename); - self::assertEquals($info->getMethod(), 'No compression'); + self::assertEquals($info->getMethodName(), 'No compression'); $zipFile->close(); } @@ -344,7 +395,7 @@ class ZipFileTest extends ZipTestCase $newName = 'tests/' . $oldName; $zipFile = new ZipFile(); - $zipFile->addDirRecursive(__DIR__); + $zipFile->addDir(__DIR__); $zipFile->saveAsFile($this->outputFilename); $zipFile->close(); @@ -406,7 +457,6 @@ class ZipFileTest extends ZipTestCase /** * @expectedException \PhpZip\Exception\ZipNotFoundEntry - * @expectedExceptionMessage Not found entry */ public function testRenameEntryNotFound() { @@ -466,7 +516,6 @@ class ZipFileTest extends ZipTestCase /** * @expectedException \PhpZip\Exception\ZipNotFoundEntry - * @expectedExceptionMessage Not found entry entry */ public function testDeleteFromNameNotFoundEntry() { @@ -482,22 +531,32 @@ class ZipFileTest extends ZipTestCase $inputDir = dirname(dirname(__DIR__)); $zipFile = new ZipFile(); - $zipFile->addFilesFromGlobRecursive($inputDir, '**.{php,xml,json}', '/'); + $zipFile->addFilesFromGlobRecursive($inputDir, '**.{xml,json,md}', '/'); + self::assertTrue(isset($zipFile['composer.json'])); + self::assertTrue(isset($zipFile['phpunit.xml'])); $zipFile->saveAsFile($this->outputFilename); $zipFile->close(); self::assertCorrectZipArchive($this->outputFilename); $zipFile->openFile($this->outputFilename); + self::assertTrue(isset($zipFile['composer.json'])); + self::assertTrue(isset($zipFile['phpunit.xml'])); $zipFile->deleteFromGlob('**.{xml,json}'); + self::assertFalse(isset($zipFile['composer.json'])); + self::assertFalse(isset($zipFile['phpunit.xml'])); $zipFile->saveAsFile($this->outputFilename); $zipFile->close(); self::assertCorrectZipArchive($this->outputFilename); $zipFile->openFile($this->outputFilename); - self::assertFalse(isset($zipFile['composer.json'])); - self::assertFalse(isset($zipFile['bootstrap.xml'])); + self::assertTrue($zipFile->count() > 0); + + foreach ($zipFile->getListFiles() as $name) { + self::assertStringEndsWith('.md', $name); + } + $zipFile->close(); } @@ -529,7 +588,7 @@ class ZipFileTest extends ZipTestCase $inputDir = dirname(dirname(__DIR__)); $zipFile = new ZipFile(); - $zipFile->addFilesFromRegexRecursive($inputDir, '~\.(xml|php|json)$~i', 'Path'); + $zipFile->addFilesFromRegexRecursive($inputDir, '~\.(xml|json)$~i', 'Path'); $zipFile->saveAsFile($this->outputFilename); $zipFile->close(); @@ -547,7 +606,7 @@ class ZipFileTest extends ZipTestCase $zipFile->openFile($this->outputFilename); self::assertFalse(isset($zipFile['Path/composer.json'])); self::assertFalse(isset($zipFile['Path/test.txt'])); - self::assertTrue(isset($zipFile['Path/bootstrap.xml'])); + self::assertTrue(isset($zipFile['Path/phpunit.xml'])); $zipFile->close(); } @@ -577,7 +636,8 @@ class ZipFileTest extends ZipTestCase public function testDeleteAll() { $zipFile = new ZipFile(); - $zipFile->addDirRecursive(__DIR__); + $zipFile->addDirRecursive(dirname(dirname(__DIR__)) .DIRECTORY_SEPARATOR. 'src'); + self::assertTrue($zipFile->count() > 0); $zipFile->saveAsFile($this->outputFilename); $zipFile->close(); @@ -733,8 +793,7 @@ class ZipFileTest extends ZipTestCase } /** - * @expectedException \PhpZip\Exception\ZipException - * @expectedExceptionMessage Not found entry + * @expectedException \PhpZip\Exception\ZipNotFoundEntry */ public function testSetEntryCommentNotFoundEntry() { @@ -750,19 +809,19 @@ class ZipFileTest extends ZipTestCase $entries = [ '1' => [ 'data' => CryptoUtil::randomBytes(255), - 'method' => ZipFile::METHOD_STORED, + 'method' => ZipFileInterface::METHOD_STORED, 'expected' => 'No compression', ], '2' => [ 'data' => CryptoUtil::randomBytes(255), - 'method' => ZipFile::METHOD_DEFLATED, + 'method' => ZipFileInterface::METHOD_DEFLATED, 'expected' => 'Deflate', ], ]; if (extension_loaded("bz2")) { $entries['3'] = [ 'data' => CryptoUtil::randomBytes(255), - 'method' => ZipFile::METHOD_BZIP2, + 'method' => ZipFileInterface::METHOD_BZIP2, 'expected' => 'Bzip2', ]; } @@ -777,12 +836,12 @@ class ZipFileTest extends ZipTestCase self::assertCorrectZipArchive($this->outputFilename); $zipFile->openFile($this->outputFilename); - $zipFile->setCompressionLevel(ZipFile::LEVEL_BEST_COMPRESSION); + $zipFile->setCompressionLevel(ZipFileInterface::LEVEL_BEST_COMPRESSION); $zipAllInfo = $zipFile->getAllInfo(); foreach ($zipAllInfo as $entryName => $info) { self::assertEquals($zipFile[$entryName], $entries[$entryName]['data']); - self::assertEquals($info->getMethod(), $entries[$entryName]['expected']); + self::assertEquals($info->getMethodName(), $entries[$entryName]['expected']); $entryInfo = $zipFile->getEntryInfo($entryName); self::assertEquals($entryInfo, $info); } @@ -948,6 +1007,10 @@ class ZipFileTest extends ZipTestCase */ public function testExtractFail3() { + if (0 === posix_getuid()) { + $this->markTestSkipped('Skip the test for a user with root privileges'); + } + $zipFile = new ZipFile(); $zipFile['file'] = 'content'; $zipFile->saveAsFile($this->outputFilename); @@ -960,105 +1023,6 @@ class ZipFileTest extends ZipTestCase $zipFile->extractTo($this->outputDirname); } - /** - * Test archive password. - */ - public function testSetPassword() - { - $password = base64_encode(CryptoUtil::randomBytes(100)); - $badPassword = "sdgt43r23wefe"; - - // create encryption password with ZipCrypto - $zipFile = new ZipFile(); - $zipFile->addDirRecursive(__DIR__); - $zipFile->withNewPassword($password, ZipFile::ENCRYPTION_METHOD_TRADITIONAL); - $zipFile->saveAsFile($this->outputFilename); - $zipFile->close(); - - self::assertCorrectZipArchive($this->outputFilename, $password); - - // check bad password for ZipCrypto - $zipFile->openFile($this->outputFilename); - $zipFile->withReadPassword($badPassword); - foreach ($zipFile->getListFiles() as $entryName) { - try { - $zipFile[$entryName]; - self::fail("Expected Exception has not been raised."); - } catch (ZipAuthenticationException $ae) { - self::assertNotNull($ae); - } - } - - // check correct password for ZipCrypto - $zipFile->withReadPassword($password); - foreach ($zipFile->getAllInfo() as $info) { - self::assertTrue($info->isEncrypted()); - self::assertContains('ZipCrypto', $info->getMethod()); - $decryptContent = $zipFile[$info->getPath()]; - self::assertNotEmpty($decryptContent); - self::assertContains('withNewPassword($password, ZipFile::ENCRYPTION_METHOD_WINZIP_AES); - $zipFile->saveAsFile($this->outputFilename); - $zipFile->close(); - - self::assertCorrectZipArchive($this->outputFilename, $password); - - // check from WinZip AES encryption - $zipFile->openFile($this->outputFilename); - // set bad password WinZip AES - $zipFile->withReadPassword($badPassword); - foreach ($zipFile->getListFiles() as $entryName) { - try { - $zipFile[$entryName]; - self::fail("Expected Exception has not been raised."); - } catch (ZipAuthenticationException $ae) { - self::assertNotNull($ae); - } - } - - // set correct password WinZip AES - $zipFile->withReadPassword($password); - foreach ($zipFile->getAllInfo() as $info) { - self::assertTrue($info->isEncrypted()); - self::assertContains('WinZip', $info->getMethod()); - $decryptContent = $zipFile[$info->getPath()]; - self::assertNotEmpty($decryptContent); - self::assertContains('addFromString('file1', ''); - $zipFile->withoutPassword(); - $zipFile->addFromString('file2', ''); - $zipFile->saveAsFile($this->outputFilename); - $zipFile->close(); - - self::assertCorrectZipArchive($this->outputFilename); - - // check remove password - $zipFile->openFile($this->outputFilename); - foreach ($zipFile->getAllInfo() as $info) { - self::assertFalse($info->isEncrypted()); - } - $zipFile->close(); - } - - /** - * @expectedException \PhpZip\Exception\ZipException - * @expectedExceptionMessage Invalid encryption method - */ - public function testSetEncryptionMethodInvalid() - { - $zipFile = new ZipFile(); - $encryptionMethod = 9999; - $zipFile->withNewPassword('pass', $encryptionMethod); - $zipFile['entry'] = 'content'; - $zipFile->outputAsString(); - } - /** * @expectedException \PhpZip\Exception\InvalidArgumentException * @expectedExceptionMessage entryName is null @@ -1101,7 +1065,7 @@ class ZipFileTest extends ZipTestCase /** * @expectedException \PhpZip\Exception\ZipUnsupportMethod - * @expectedExceptionMessage Unsupported method + * @expectedExceptionMessage Unsupported compression method */ public function testAddFromStringUnsupportedMethod() { @@ -1142,8 +1106,8 @@ class ZipFileTest extends ZipTestCase $zipFile->openFile($this->outputFilename); $infoStored = $zipFile->getEntryInfo(basename($fileStored)); $infoDeflated = $zipFile->getEntryInfo(basename($fileDeflated)); - self::assertEquals($infoStored->getMethod(), 'No compression'); - self::assertEquals($infoDeflated->getMethod(), 'Deflate'); + self::assertEquals($infoStored->getMethodName(), 'No compression'); + self::assertEquals($infoDeflated->getMethodName(), 'Deflate'); $zipFile->close(); } @@ -1154,6 +1118,7 @@ class ZipFileTest extends ZipTestCase public function testAddFromStreamInvalidResource() { $zipFile = new ZipFile(); + /** @noinspection PhpParamsInspection */ $zipFile->addFromStream("invalid resource", "name"); } @@ -1207,8 +1172,8 @@ class ZipFileTest extends ZipTestCase $zipFile->openFile($this->outputFilename); $infoStored = $zipFile->getEntryInfo(basename($fileStored)); $infoDeflated = $zipFile->getEntryInfo(basename($fileDeflated)); - self::assertEquals($infoStored->getMethod(), 'No compression'); - self::assertEquals($infoDeflated->getMethod(), 'Deflate'); + self::assertEquals($infoStored->getMethodName(), 'No compression'); + self::assertEquals($infoDeflated->getMethodName(), 'Deflate'); $zipFile->close(); } @@ -1248,6 +1213,10 @@ class ZipFileTest extends ZipTestCase */ public function testAddFileCantOpen() { + if (0 === posix_getuid()) { + $this->markTestSkipped('Skip the test for a user with root privileges'); + } + self::assertNotFalse(file_put_contents($this->outputFilename, '')); self::assertTrue(chmod($this->outputFilename, 0244)); @@ -1522,6 +1491,7 @@ class ZipFileTest extends ZipTestCase public function testSaveAsStreamBadStream() { $zipFile = new ZipFile(); + /** @noinspection PhpParamsInspection */ $zipFile->saveAsStream("bad stream"); } @@ -1531,6 +1501,10 @@ class ZipFileTest extends ZipTestCase */ public function testSaveAsFileNotWritable() { + if (0 === posix_getuid()) { + $this->markTestSkipped('Skip the test for a user with root privileges'); + } + self::assertTrue(mkdir($this->outputDirname, 0444, true)); self::assertTrue(chmod($this->outputDirname, 0444)); @@ -1551,13 +1525,13 @@ class ZipFileTest extends ZipTestCase $files['file' . $i . '.txt'] = CryptoUtil::randomBytes(255); } - $methods = [ZipFile::METHOD_STORED, ZipFile::METHOD_DEFLATED]; + $methods = [ZipFileInterface::METHOD_STORED, ZipFileInterface::METHOD_DEFLATED]; if (extension_loaded("bz2")) { - $methods[] = ZipFile::METHOD_BZIP2; + $methods[] = ZipFileInterface::METHOD_BZIP2; } $zipFile = new ZipFile(); - $zipFile->setCompressionLevel(ZipFile::LEVEL_BEST_SPEED); + $zipFile->setCompressionLevel(ZipFileInterface::LEVEL_BEST_SPEED); foreach ($files as $entryName => $content) { $zipFile->addFromString($entryName, $content, $methods[array_rand($methods)]); } @@ -1628,18 +1602,43 @@ class ZipFileTest extends ZipTestCase public function testArrayAccessAddFile() { $entryName = 'path/to/file.dat'; + $entryNameStream = 'path/to/' . basename(__FILE__); $zipFile = new ZipFile(); $zipFile[$entryName] = new \SplFileInfo(__FILE__); + $zipFile[$entryNameStream] = fopen(__FILE__, 'r'); $zipFile->saveAsFile($this->outputFilename); $zipFile->close(); self::assertCorrectZipArchive($this->outputFilename); $zipFile->openFile($this->outputFilename); - self::assertEquals(sizeof($zipFile), 1); + self::assertEquals(sizeof($zipFile), 2); self::assertTrue(isset($zipFile[$entryName])); + self::assertTrue(isset($zipFile[$entryNameStream])); self::assertEquals($zipFile[$entryName], file_get_contents(__FILE__)); + self::assertEquals($zipFile[$entryNameStream], file_get_contents(__FILE__)); + $zipFile->close(); + } + + public function testUnknownCompressionMethod() + { + $zipFile = new ZipFile(); + + $zipFile->addFromString('file', 'content', ZipEntry::UNKNOWN); + $zipFile->addFromString('file2', base64_encode(CryptoUtil::randomBytes(512)), ZipEntry::UNKNOWN); + + self::assertEquals($zipFile->getEntryInfo('file')->getMethodName(), 'Unknown'); + self::assertEquals($zipFile->getEntryInfo('file2')->getMethodName(), 'Unknown'); + + $zipFile->saveAsFile($this->outputFilename); + $zipFile->close(); + + $zipFile->openFile($this->outputFilename); + + self::assertEquals($zipFile->getEntryInfo('file')->getMethodName(), 'No compression'); + self::assertEquals($zipFile->getEntryInfo('file2')->getMethodName(), 'Deflate'); + $zipFile->close(); } @@ -1663,26 +1662,6 @@ class ZipFileTest extends ZipTestCase $zipFile->addEmptyDir(""); } - /** - * @expectedException \PhpZip\Exception\InvalidArgumentException - * @expectedExceptionMessage Output filename is empty. - */ - public function testOutputAsAttachmentNullName() - { - $zipFile = new ZipFile(); - $zipFile->outputAsAttachment(null); - } - - /** - * @expectedException \PhpZip\Exception\InvalidArgumentException - * @expectedExceptionMessage Output filename is empty. - */ - public function testOutputAsAttachmentEmptyName() - { - $zipFile = new ZipFile(); - $zipFile->outputAsAttachment(''); - } - /** * @expectedException \PhpZip\Exception\ZipNotFoundEntry * @expectedExceptionMessage Zip entry bad entry name not found @@ -1701,8 +1680,10 @@ class ZipFileTest extends ZipTestCase $zipFile = new ZipFile(); $zipFile['file'] = 'content'; $zipFile['file2'] = 'content2'; - $zipFile->saveAsFile($this->outputFilename); - $zipFile->close(); + self::assertEquals(count($zipFile), 2); + $zipFile + ->saveAsFile($this->outputFilename) + ->close(); $md5file = md5_file($this->outputFilename); @@ -1711,7 +1692,7 @@ class ZipFileTest extends ZipTestCase self::assertTrue(isset($zipFile['file'])); self::assertTrue(isset($zipFile['file2'])); $zipFile['file3'] = 'content3'; - self::assertEquals(count($zipFile), 2); + self::assertEquals(count($zipFile), 3); $zipFile = $zipFile->rewrite(); self::assertEquals(count($zipFile), 3); self::assertTrue(isset($zipFile['file'])); @@ -1759,14 +1740,14 @@ class ZipFileTest extends ZipTestCase /** * Test zip alignment. */ - public function testZipAlign() + public function testZipAlignSourceZip() { $zipFile = new ZipFile(); for ($i = 0; $i < 100; $i++) { $zipFile->addFromString( 'entry' . $i . '.txt', CryptoUtil::randomBytes(mt_rand(100, 4096)), - ZipFile::METHOD_STORED + ZipFileInterface::METHOD_STORED ); } $zipFile->saveAsFile($this->outputFilename); @@ -1775,7 +1756,9 @@ class ZipFileTest extends ZipTestCase self::assertCorrectZipArchive($this->outputFilename); $result = self::doZipAlignVerify($this->outputFilename); - if ($result === null) return; // zip align not installed + if ($result === null) { + return; + } // zip align not installed // check not zip align self::assertFalse($result); @@ -1792,13 +1775,16 @@ class ZipFileTest extends ZipTestCase // check zip align self::assertTrue($result); + } + public function testZipAlignNewFiles() + { $zipFile = new ZipFile(); for ($i = 0; $i < 100; $i++) { $zipFile->addFromString( 'entry' . $i . '.txt', CryptoUtil::randomBytes(mt_rand(100, 4096)), - ZipFile::METHOD_STORED + ZipFileInterface::METHOD_STORED ); } $zipFile->setZipAlign(4); @@ -1808,20 +1794,291 @@ class ZipFileTest extends ZipTestCase self::assertCorrectZipArchive($this->outputFilename); $result = self::doZipAlignVerify($this->outputFilename); + if ($result === null) { + return; + } // zip align not installed // check not zip align self::assertTrue($result); } - public function 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 @@ class ZipFileTest extends ZipTestCase $zipFile->openFile($this->outputFilename); self::assertEquals($zipFile->count(), $countFiles); + $i = 0; foreach ($zipFile as $entry => $content) { - + self::assertEquals($entry, $i . '.txt'); + self::assertEquals($content, $i); + $i++; } $zipFile->close(); } - -} \ No newline at end of file +} diff --git a/tests/PhpZip/ZipMatcherTest.php b/tests/PhpZip/ZipMatcherTest.php new file mode 100644 index 0000000..c824765 --- /dev/null +++ b/tests/PhpZip/ZipMatcherTest.php @@ -0,0 +1,111 @@ +matcher(); + self::assertInstanceOf(ZipEntryMatcher::class, $matcher); + + $this->assertTrue(is_array($matcher->getMatches())); + $this->assertCount(0, $matcher); + + $matcher->add(1)->add(10)->add(20); + $this->assertCount(3, $matcher); + $this->assertEquals($matcher->getMatches(), ['1', '10', '20']); + + $matcher->delete(); + $this->assertCount(97, $zipFile); + $this->assertCount(0, $matcher); + + $matcher->match('~^[2][1-5]|[3][6-9]|40$~s'); + $this->assertCount(10, $matcher); + $actualMatches = [ + '21', '22', '23', '24', '25', + '36', '37', '38', '39', + '40' + ]; + $this->assertEquals($matcher->getMatches(), $actualMatches); + $matcher->setPassword('qwerty'); + $info = $zipFile->getAllInfo(); + array_walk($info, function (ZipInfo $zipInfo) use ($actualMatches) { + self::assertEquals($zipInfo->isEncrypted(), in_array($zipInfo->getName(), $actualMatches)); + }); + + $matcher->all(); + $this->assertCount(count($zipFile), $matcher); + + $expectedNames = []; + $matcher->invoke(function ($entryName) use (&$expectedNames) { + $expectedNames[] = $entryName; + }); + $this->assertEquals($expectedNames, $matcher->getMatches()); + + $zipFile->close(); + } + + public function testDocsExample() + { + $zipFile = new ZipFile(); + for ($i = 0; $i < 100; $i++) { + $zipFile['file_'.$i.'.jpg'] = CryptoUtil::randomBytes(100); + } + + $renameEntriesArray = [ + 'file_10.jpg', + 'file_11.jpg', + 'file_12.jpg', + 'file_13.jpg', + 'file_14.jpg', + 'file_15.jpg', + 'file_16.jpg', + 'file_17.jpg', + 'file_18.jpg', + 'file_19.jpg', + 'file_50.jpg', + 'file_51.jpg', + 'file_52.jpg', + 'file_53.jpg', + 'file_54.jpg', + 'file_55.jpg', + 'file_56.jpg', + 'file_57.jpg', + 'file_58.jpg', + 'file_59.jpg', + ]; + + foreach ($renameEntriesArray as $name) { + self::assertTrue(isset($zipFile[$name])); + } + + $matcher = $zipFile->matcher(); + $matcher->match('~^file_(1|5)\d+~'); + self::assertEquals($matcher->getMatches(), $renameEntriesArray); + + $matcher->invoke(function ($entryName) use ($zipFile) { + $newName = preg_replace('~\.(jpe?g)$~i', '.no_optimize.$1', $entryName); + $zipFile->rename($entryName, $newName); + }); + + foreach ($renameEntriesArray as $name) { + self::assertFalse(isset($zipFile[$name])); + + $pathInfo = pathinfo($name); + $newName = $pathInfo['filename'].'.no_optimize.'.$pathInfo['extension']; + self::assertTrue(isset($zipFile[$newName])); + } + + $zipFile->close(); + } +} diff --git a/tests/PhpZip/ZipPasswordTest.php b/tests/PhpZip/ZipPasswordTest.php new file mode 100644 index 0000000..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 @@ class ZipTestCase extends \PHPUnit_Framework_TestCase { if (DIRECTORY_SEPARATOR !== '\\' && `which zipalign`) { exec("zipalign -c -v 4 " . escapeshellarg($filename), $output, $returnCode); - if ($showErrors && $returnCode !== 0) fwrite(STDERR, implode(PHP_EOL, $output)); + if ($showErrors && $returnCode !== 0) { + fwrite(STDERR, implode(PHP_EOL, $output)); + } return $returnCode === 0; } else { - fwrite(STDERR, 'Can not find program "zipalign" for test'); + fwrite(STDERR, 'Can not find program "zipalign" for test' . PHP_EOL); return null; } } - -} \ No newline at end of file +} diff --git a/tests/PhpZip/php-zip-ext-test-resources/binarynull.zip b/tests/PhpZip/php-zip-ext-test-resources/binarynull.zip new file mode 100644 index 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私はガラスを食べられます.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/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