diff --git a/.gitattributes b/.gitattributes index ac04ecc..53d0202 100644 --- a/.gitattributes +++ b/.gitattributes @@ -2,5 +2,6 @@ .github export-ignore .gitignore export-ignore .travis.yml export-ignore +.php_cs export-ignore phpunit.xml export-ignore -tests export-ignore \ No newline at end of file +tests export-ignore diff --git a/.php_cs b/.php_cs index 001d019..5f25716 100644 --- a/.php_cs +++ b/.php_cs @@ -232,9 +232,10 @@ $rules = [ 'mute_deprecation_error' => true, 'noise_remaining_usages' => true, 'noise_remaining_usages_exclude' => [ - 'gzinflate', 'fclose', 'fopen', + 'gzinflate', + 'iconv', 'mime_content_type', 'rename', 'unlink', diff --git a/.phpstorm.meta.php b/.phpstorm.meta.php new file mode 100644 index 0000000..3b911f9 --- /dev/null +++ b/.phpstorm.meta.php @@ -0,0 +1,62 @@ +addFromString("zip/entry/filename", "Is file content") // добавить запись из строки - ->addFile("/path/to/file", "data/tofile") // добавить запись из файла - ->addDir(__DIR__, "to/path/") // добавить файлы из директории + ->addFromString('zip/entry/filename', "Is file content") // добавить запись из строки + ->addFile('/path/to/file', 'data/tofile') // добавить запись из файла + ->addDir(__DIR__, 'to/path/') // добавить файлы из директории ->saveAsFile($outputFilename) // сохранить архив в файл ->close(); // закрыть архив @@ -115,6 +115,8 @@ finally{ - [ZipFile::addDirRecursive](#Documentation-ZipFile-addDirRecursive) - добавляет файлы из директории по указанному пути c вложенными директориями. - [ZipFile::addEmptyDir](#Documentation-ZipFile-addEmptyDir) - добавляет в ZIP-архив новую директорию. - [ZipFile::addFile](#Documentation-ZipFile-addFile) - добавляет в ZIP-архив файл по указанному пути. +- [ZipFile::addSplFile](#Documentation-ZipFile-addSplFile) - добавляет объект `\SplFileInfo` в zip-архив. +- [ZipFile::addFromFinder](#Documentation-ZipFile-addFromFinder) - добавляет файлы из `Symfony\Component\Finder\Finder` в zip архив. - [ZipFile::addFilesFromIterator](#Documentation-ZipFile-addFilesFromIterator) - добавляет файлы из итератора директорий. - [ZipFile::addFilesFromGlob](#Documentation-ZipFile-addFilesFromGlob) - добавляет файлы из директории в соответствии с glob шаблоном без вложенных директорий. - [ZipFile::addFilesFromGlobRecursive](#Documentation-ZipFile-addFilesFromGlobRecursive) - добавляет файлы из директории в соответствии с glob шаблоном c вложенными директориями. @@ -192,12 +194,15 @@ $zipFile->openFromStream($stream); #### Чтение записей из архива **ZipFile::count** - возвращает количество записей в архиве. ```php +$zipFile = new \PhpZip\ZipFile(); + $count = count($zipFile); // или $count = $zipFile->count(); ``` **ZipFile::getListFiles** - возвращает список файлов архива. ```php +$zipFile = new \PhpZip\ZipFile(); $listFiles = $zipFile->getListFiles(); // Пример содержимого массива: @@ -210,6 +215,7 @@ $listFiles = $zipFile->getListFiles(); **ZipFile::getEntryContent** - возвращает содержимое записи. ```php // $entryName = 'path/to/example-entry-name.txt'; +$zipFile = new \PhpZip\ZipFile(); $contents = $zipFile[$entryName]; // или @@ -218,6 +224,7 @@ $contents = $zipFile->getEntryContents($entryName); **ZipFile::hasEntry** - проверяет, присутствует ли запись в архиве. ```php // $entryName = 'path/to/example-entry-name.txt'; +$zipFile = new \PhpZip\ZipFile(); $hasEntry = isset($zipFile[$entryName]); // или @@ -226,23 +233,26 @@ $hasEntry = $zipFile->hasEntry($entryName); **ZipFile::isDirectory** - проверяет, является ли запись в архиве директорией. ```php // $entryName = 'path/to/'; +$zipFile = new \PhpZip\ZipFile(); $isDirectory = $zipFile->isDirectory($entryName); ``` **ZipFile::extractTo** - извлекает содержимое архива в заданную директорию. Директория должна существовать. ```php +$zipFile = new \PhpZip\ZipFile(); $zipFile->extractTo($directory); ``` Можно извлечь только некоторые записи в заданную директорию. Директория должна существовать. ```php $extractOnlyFiles = [ - "filename1", - "filename2", - "dir/dir/dir/" + 'filename1', + 'filename2', + 'dir/dir/dir/' ]; -$zipFile->extractTo($directory, $extractOnlyFiles); +$zipFile = new \PhpZip\ZipFile(); +$zipFile->extractTo($toDirectory, $extractOnlyFiles); ``` #### Перебор записей/Итератор `ZipFile` является итератором. @@ -251,7 +261,7 @@ $zipFile->extractTo($directory, $extractOnlyFiles); foreach($zipFile as $entryName => $contents){ echo "Файл: $entryName" . PHP_EOL; echo "Содержимое: $contents" . PHP_EOL; - echo "-----------------------------" . PHP_EOL; + echo '-----------------------------' . PHP_EOL; } ``` Можно использовать паттерн `Iterator`. @@ -264,7 +274,7 @@ while ($iterator->valid()) echo "Файл: $entryName" . PHP_EOL; echo "Содержимое: $contents" . PHP_EOL; - echo "-----------------------------" . PHP_EOL; + echo '-----------------------------' . PHP_EOL; $iterator->next(); } @@ -280,110 +290,85 @@ $commentEntry = $zipFile->getEntryComment($entryName); ``` **ZipFile::getEntryInfo** - возвращает подробную информацию о записи в архиве. ```php +$zipFile = new \PhpZip\ZipFile(); $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` +- `\PhpZip\Constants\ZipCompressionMethod::STORED` - без сжатия +- `\PhpZip\Constants\ZipCompressionMethod::DEFLATED` - Deflate сжатие +- `\PhpZip\Constants\ZipCompressionMethod::BZIP2` - Bzip2 сжатие при наличии расширения `ext-bz2` **ZipFile::addFile** - добавляет в ZIP-архив файл по указанному пути из файловой системы. ```php +$zipFile = new \PhpZip\ZipFile(); // $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->addFile($file, $entryName, \PhpZip\Constants\ZipCompressionMethod::STORED); // Без сжатия +$zipFile->addFile($file, $entryName, \PhpZip\Constants\ZipCompressionMethod::DEFLATED); // Deflate сжатие +$zipFile->addFile($file, $entryName, \PhpZip\Constants\ZipCompressionMethod::BZIP2); // BZIP2 сжатие +``` + +**ZipFile::addSplFile"** - добавляет объект `\SplFileInfo` в zip-архив. +```php +// $file = '...../file.ext'; +// $entryName = 'file2.ext' +$zipFile = new \PhpZip\ZipFile(); + +$splFile = new \SplFileInfo('README.md'); + +$zipFile->addSplFile($splFile); +$zipFile->addSplFile($splFile, $entryName); +// or +$zipFile[$entryName] = new \SplFileInfo($file); + +// установить метод сжатия +$zipFile->addSplFile($splFile, $entryName, $options = [ + \PhpZip\Constants\ZipOptions::COMPRESSION_METHOD => \PhpZip\Constants\ZipCompressionMethod::DEFLATED, +]); +``` + +**ZipFile::addFromFinder"** - добавляет файлы из `Symfony\Component\Finder\Finder` в zip архив. +https://symfony.com/doc/current/components/finder.html +```php +$finder = new \Symfony\Component\Finder\Finder(); +$finder + ->files() + ->name('*.{jpg,jpeg,gif,png}') + ->name('/^[0-9a-f]\./') + ->contains('/lorem\s+ipsum$/i') + ->in('path'); + +$zipFile = new \PhpZip\ZipFile(); +$zipFile->addFromFinder($finder, $options = [ + \PhpZip\Constants\ZipOptions::COMPRESSION_METHOD => \PhpZip\Constants\ZipCompressionMethod::DEFLATED, + \PhpZip\Constants\ZipOptions::MODIFIED_TIME => new \DateTimeImmutable('-1 day 5 min') +]); ``` **ZipFile::addFromString** - добавляет файл в ZIP-архив, используя его содержимое в виде строки. ```php +$zipFile = new \PhpZip\ZipFile(); + $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->addFromString($entryName, $contents, \PhpZip\Constants\ZipCompressionMethod::STORED); // Без сжатия +$zipFile->addFromString($entryName, $contents, \PhpZip\Constants\ZipCompressionMethod::DEFLATED); // Deflate сжатие +$zipFile->addFromString($entryName, $contents, \PhpZip\Constants\ZipCompressionMethod::BZIP2); // BZIP2 сжатие ``` **ZipFile::addFromStream** - добавляет в ZIP-архив запись из потока. ```php @@ -392,9 +377,9 @@ $zipFile->addFromString($entryName, $contents, ZipFile::METHOD_BZIP2); // BZIP2 $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->addFromStream($stream, $entryName, \PhpZip\Constants\ZipCompressionMethod::STORED); // Без сжатия +$zipFile->addFromStream($stream, $entryName, \PhpZip\Constants\ZipCompressionMethod::DEFLATED); // Deflate сжатие +$zipFile->addFromStream($stream, $entryName, \PhpZip\Constants\ZipCompressionMethod::BZIP2); // BZIP2 сжатие ``` **ZipFile::addEmptyDir** - добавляет в ZIP-архив новую (пустую) директорию. ```php @@ -424,9 +409,9 @@ $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->addDir($dirName, $localPath, \PhpZip\Constants\ZipCompressionMethod::STORED); // Без сжатия +$zipFile->addDir($dirName, $localPath, \PhpZip\Constants\ZipCompressionMethod::DEFLATED); // Deflate сжатие +$zipFile->addDir($dirName, $localPath, \PhpZip\Constants\ZipCompressionMethod::BZIP2); // BZIP2 сжатие ``` **ZipFile::addDirRecursive** - добавляет файлы из директории по указанному пути c вложенными директориями. ```php @@ -437,9 +422,9 @@ $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->addDirRecursive($dirName, $localPath, \PhpZip\Constants\ZipCompressionMethod::STORED); // Без сжатия +$zipFile->addDirRecursive($dirName, $localPath, \PhpZip\Constants\ZipCompressionMethod::DEFLATED); // Deflate сжатие +$zipFile->addDirRecursive($dirName, $localPath, \PhpZip\Constants\ZipCompressionMethod::BZIP2); // BZIP2 сжатие ``` **ZipFile::addFilesFromIterator** - добавляет файлы из итератора директорий. ```php @@ -455,9 +440,9 @@ $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 сжатие +$zipFile->addFilesFromIterator($directoryIterator, $localPath, \PhpZip\Constants\ZipCompressionMethod::STORED); // Без сжатия +$zipFile->addFilesFromIterator($directoryIterator, $localPath, \PhpZip\Constants\ZipCompressionMethod::DEFLATED); // Deflate сжатие +$zipFile->addFilesFromIterator($directoryIterator, $localPath, \PhpZip\Constants\ZipCompressionMethod::BZIP2); // BZIP2 сжатие ``` Пример добавления файлов из директории в архив с игнорированием некоторых файлов при помощи итератора директорий. ```php @@ -488,9 +473,9 @@ $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->addFilesFromGlob($dir, $globPattern, $localPath, \PhpZip\Constants\ZipCompressionMethod::STORED); // Без сжатия +$zipFile->addFilesFromGlob($dir, $globPattern, $localPath, \PhpZip\Constants\ZipCompressionMethod::DEFLATED); // Deflate сжатие +$zipFile->addFilesFromGlob($dir, $globPattern, $localPath, \PhpZip\Constants\ZipCompressionMethod::BZIP2); // BZIP2 сжатие ``` **ZipFile::addFilesFromGlobRecursive** - добавляет файлы из директории в соответствии с [glob шаблоном](https://en.wikipedia.org/wiki/Glob_(programming)) c вложенными директориями. ```php @@ -503,9 +488,9 @@ $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->addFilesFromGlobRecursive($dir, $globPattern, $localPath, \PhpZip\Constants\ZipCompressionMethod::STORED); // Без сжатия +$zipFile->addFilesFromGlobRecursive($dir, $globPattern, $localPath, \PhpZip\Constants\ZipCompressionMethod::DEFLATED); // Deflate сжатие +$zipFile->addFilesFromGlobRecursive($dir, $globPattern, $localPath, \PhpZip\Constants\ZipCompressionMethod::BZIP2); // BZIP2 сжатие ``` **ZipFile::addFilesFromRegex** - добавляет файлы из директории в соответствии с [регулярным выражением](https://en.wikipedia.org/wiki/Regular_expression) без вложенных директорий. ```php @@ -518,9 +503,9 @@ $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->addFilesFromRegex($dir, $regexPattern, $localPath, \PhpZip\Constants\ZipCompressionMethod::STORED); // Без сжатия +$zipFile->addFilesFromRegex($dir, $regexPattern, $localPath, \PhpZip\Constants\ZipCompressionMethod::DEFLATED); // Deflate сжатие +$zipFile->addFilesFromRegex($dir, $regexPattern, $localPath, \PhpZip\Constants\ZipCompressionMethod::BZIP2); // BZIP2 сжатие ``` **ZipFile::addFilesFromRegexRecursive** - добавляет файлы из директории в соответствии с [регулярным выражением](https://en.wikipedia.org/wiki/Regular_expression) с вложенными директориями. ```php @@ -533,9 +518,9 @@ $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->addFilesFromRegexRecursive($dir, $regexPattern, $localPath, \PhpZip\Constants\ZipCompressionMethod::STORED); // Без сжатия +$zipFile->addFilesFromRegexRecursive($dir, $regexPattern, $localPath, \PhpZip\Constants\ZipCompressionMethod::DEFLATED); // Deflate сжатие +$zipFile->addFilesFromRegexRecursive($dir, $regexPattern, $localPath, \PhpZip\Constants\ZipCompressionMethod::BZIP2); // BZIP2 сжатие ``` #### Удаление записей из архива **ZipFile::deleteFromName** - удаляет запись по имени. @@ -567,26 +552,26 @@ $zipFile->rename($oldName, $newName); > _Обратите внимание, что действие данного метода не распространяется на записи, добавленные после выполнения этого метода._ -По умолчанию используется уровень сжатия -1 (`\PhpZip\ZipFile::LEVEL_DEFAULT_COMPRESSION`) или уровень сжатия, определённый в архиве для Deflate сжатия. +По умолчанию используется уровень сжатия 5 (`\PhpZip\Constants\ZipCompressionLevel::NORMAL`) или уровень сжатия, определённый в архиве для Deflate сжатия. -Поддерживаются значения -1 (`\PhpZip\ZipFile::LEVEL_DEFAULT_COMPRESSION`) и диапазон от 1 (`\PhpZip\ZipFile::LEVEL_BEST_SPEED`) до 9 (`\PhpZip\ZipFile::LEVEL_BEST_COMPRESSION`). Чем выше число, тем лучше и дольше сжатие. +Поддерживаются диапазон значений от 1 (`\PhpZip\Constants\ZipCompressionLevel::SUPER_FAST`) до 9 (`\PhpZip\Constants\ZipCompressionLevel::MAXIMUM`). Чем выше число, тем лучше и дольше сжатие. ```php -$zipFile->setCompressionLevel(\PhpZip\ZipFile::LEVEL_BEST_COMPRESSION); +$zipFile->setCompressionLevel(\PhpZip\Constants\ZipCompressionLevel::MAXIMUM); ``` **ZipFile::setCompressionLevelEntry** - устанавливает уровень сжатия для определённой записи в архиве. -Поддерживаются значения -1 (`\PhpZip\ZipFile::LEVEL_DEFAULT_COMPRESSION`) и диапазон от 1 (`\PhpZip\ZipFile::LEVEL_BEST_SPEED`) до 9 (`\PhpZip\ZipFile::LEVEL_BEST_COMPRESSION`). Чем выше число, тем лучше и дольше сжатие. +Поддерживаются диапазон значений от 1 (`\PhpZip\Constants\ZipCompressionLevel::SUPER_FAST`) до 9 (`\PhpZip\Constants\ZipCompressionLevel::MAXIMUM`). Чем выше число, тем лучше и дольше сжатие. ```php -$zipFile->setCompressionLevelEntry($entryName, \PhpZip\ZipFile::LEVEL_BEST_COMPRESSION); +$zipFile->setCompressionLevelEntry($entryName, \PhpZip\Constants\ZipCompressionLevel::MAXIMUM); ``` **ZipFile::setCompressionMethodEntry** - устанавливает метод сжатия для определённой записи в архиве. Доступны следующие методы сжатия: -- `\PhpZip\ZipFile::METHOD_STORED` - без сжатия -- `\PhpZip\ZipFile::METHOD_DEFLATED` - Deflate сжатие -- `\PhpZip\ZipFile::METHOD_BZIP2` - Bzip2 сжатие при наличии расширения `ext-bz2` +- `\PhpZip\Constants\ZipCompressionMethod::STORED` - без сжатия +- `\PhpZip\Constants\ZipCompressionMethod::DEFLATED` - Deflate сжатие +- `\PhpZip\Constants\ZipCompressionMethod::BZIP2` - Bzip2 сжатие при наличии расширения `ext-bz2` ```php -$zipFile->setCompressionMethodEntry($entryName, ZipFile::METHOD_DEFLATED); +$zipFile->setCompressionMethodEntry($entryName, \PhpZip\Constants\ZipCompressionMethod::DEFLATED); ``` **ZipFile::setArchiveComment** - устанавливает комментарий к ZIP-архиву. ```php @@ -652,10 +637,10 @@ $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 +- `\PhpZip\Constants\ZipEncryptionMethod::PKWARE` - Traditional PKWARE encryption +- `\PhpZip\Constants\ZipEncryptionMethod::WINZIP_AES_256` - WinZip AES encryption 256 bit (рекомендуемое) +- `\PhpZip\Constants\ZipEncryptionMethod::WINZIP_AES_192` - WinZip AES encryption 192 bit +- `\PhpZip\Constants\ZipEncryptionMethod::WINZIP_AES_128` - WinZip AES encryption 128 bit **ZipFile::setReadPassword** - устанавливает пароль на чтение открытого запароленного архива для всех зашифрованных записей. @@ -675,7 +660,7 @@ $zipFile->setPassword($password); ``` Можно установить метод шифрования: ```php -$encryptionMethod = ZipFile::ENCRYPTION_METHOD_WINZIP_AES_256; +$encryptionMethod = \PhpZip\Constants\ZipEncryptionMethod::WINZIP_AES_256; $zipFile->setPassword($password, $encryptionMethod); ``` **ZipFile::setPasswordEntry** - устанавливает новый пароль для конкретного файла. @@ -684,7 +669,7 @@ $zipFile->setPasswordEntry($entryName, $password); ``` Можно установить метод шифрования: ```php -$encryptionMethod = ZipFile::ENCRYPTION_METHOD_WINZIP_AES_256; +$encryptionMethod = \PhpZip\Constants\ZipEncryptionMethod::WINZIP_AES_256; $zipFile->setPasswordEntry($entryName, $password, $encryptionMethod); ``` **ZipFile::disableEncryption** - отключает шифрования всех записей, находящихся в архиве. @@ -789,7 +774,8 @@ composer install --dev vendor/bin/phpunit -v -c phpunit.xml ``` ### История изменений -[Ссылка на Changelog](CHANGELOG.md) +История изменений на [странице релизов](https://github.com/Ne-Lexa/php-zip/releases). + ### Обновление версий #### Обновление с версии 2 до версии 3.0 Обновите мажорную версию в файле `composer.json` до `^3.0`. diff --git a/README.md b/README.md index 6021757..fa712f9 100644 --- a/README.md +++ b/README.md @@ -80,9 +80,9 @@ Latest stable version: [![Latest Stable Version](https://poser.pugx.org/nelexa/z $zipFile = new \PhpZip\ZipFile(); try{ $zipFile - ->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 + ->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 @@ -115,6 +115,8 @@ Other examples can be found in the `tests/` folder - [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::addSplFile](#Documentation-ZipFile-addSplFile) - adds a `\SplFileInfo` to a ZIP archive. +- [ZipFile::addFromFinder](#Documentation-ZipFile-addFromFinder) - adds files from the `Symfony\Component\Finder\Finder` to a ZIP archive. - [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. @@ -192,12 +194,15 @@ $zipFile->openFromStream($stream); #### Reading entries from the archive **ZipFile::count** - returns the number of entries in the archive. ```php +$zipFile = new \PhpZip\ZipFile(); + $count = count($zipFile); // or $count = $zipFile->count(); ``` **ZipFile::getListFiles** - returns list of archive files. ```php +$zipFile = new \PhpZip\ZipFile(); $listFiles = $zipFile->getListFiles(); // example array contents: @@ -205,11 +210,13 @@ $listFiles = $zipFile->getListFiles(); // 0 => 'info.txt', // 1 => 'path/to/file.jpg', // 2 => 'another path/', +// 3 => '0', // ) ``` **ZipFile::getEntryContent** - returns the entry contents using its name. ```php // $entryName = 'path/to/example-entry-name.txt'; +$zipFile = new \PhpZip\ZipFile(); $contents = $zipFile[$entryName]; // or @@ -218,6 +225,7 @@ $contents = $zipFile->getEntryContents($entryName); **ZipFile::hasEntry** - checks if there is an entry in the archive. ```php // $entryName = 'path/to/example-entry-name.txt'; +$zipFile = new \PhpZip\ZipFile(); $hasEntry = isset($zipFile[$entryName]); // or @@ -226,23 +234,27 @@ $hasEntry = $zipFile->hasEntry($entryName); **ZipFile::isDirectory** - checks that the entry in the archive is a directory. ```php // $entryName = 'path/to/'; +$zipFile = new \PhpZip\ZipFile(); $isDirectory = $zipFile->isDirectory($entryName); ``` **ZipFile::extractTo** - extract the archive contents. The directory must exist. ```php +$zipFile = new \PhpZip\ZipFile(); $zipFile->extractTo($directory); ``` Extract some files to the directory. The directory must exist. ```php +// $toDirectory = '/tmp'; $extractOnlyFiles = [ - "filename1", - "filename2", - "dir/dir/dir/" + 'filename1', + 'filename2', + 'dir/dir/dir/' ]; -$zipFile->extractTo($directory, $extractOnlyFiles); +$zipFile = new \PhpZip\ZipFile(); +$zipFile->extractTo($toDirectory, $extractOnlyFiles); ``` #### Iterating entries `ZipFile` is an iterator. @@ -251,7 +263,7 @@ Can iterate all the entries in the `foreach` loop. foreach($zipFile as $entryName => $contents){ echo "Filename: $entryName" . PHP_EOL; echo "Contents: $contents" . PHP_EOL; - echo "-----------------------------" . PHP_EOL; + echo '-----------------------------' . PHP_EOL; } ``` Can iterate through the `Iterator`. @@ -264,7 +276,7 @@ while ($iterator->valid()) echo "Filename: $entryName" . PHP_EOL; echo "Contents: $contents" . PHP_EOL; - echo "-----------------------------" . PHP_EOL; + echo '-----------------------------' . PHP_EOL; $iterator->next(); } @@ -272,121 +284,100 @@ while ($iterator->valid()) #### Getting information about entries **ZipFile::getArchiveComment** - returns the Zip archive comment. ```php +$zipFile = new \PhpZip\ZipFile(); $commentArchive = $zipFile->getArchiveComment(); ``` **ZipFile::getEntryComment** - returns the comment of an entry using the entry name. ```php +$zipFile = new \PhpZip\ZipFile(); $commentEntry = $zipFile->getEntryComment($entryName); ``` **ZipFile::getEntryInfo** - returns detailed information about the entry in the archive ```php +$zipFile = new \PhpZip\ZipFile(); $zipInfo = $zipFile->getEntryInfo('file.txt'); - -$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 {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** - returns detailed information about all entries in the archive. ```php $zipAllInfo = $zipFile->getAllInfo(); - -print_r($zipAllInfo); -//Array -//( -// [file.txt] => PhpZip\Model\ZipInfo Object -// ( -// ... -// ) -// -// [file2.txt] => PhpZip\Model\ZipInfo Object -// ( -// ... -// ) -// -// ... -//) ``` #### 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` +- `\PhpZip\Constants\ZipCompressionMethod::STORED` - no compression +- `\PhpZip\Constants\ZipCompressionMethod::DEFLATED` - Deflate compression +- `\PhpZip\Constants\ZipCompressionMethod::BZIP2` - Bzip2 compression with the extension `ext-bz2` **ZipFile::addFile** - adds a file to a ZIP archive from the given path. ```php +$zipFile = new \PhpZip\ZipFile(); // $file = '...../file.ext'; +// $entryName = 'file2.ext' $zipFile->addFile($file); // 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); + +// you can specify a compression method +$zipFile->addFile($file, $entryName, \PhpZip\Constants\ZipCompressionMethod::STORED); // No compression +$zipFile->addFile($file, $entryName, \PhpZip\Constants\ZipCompressionMethod::DEFLATED); // Deflate compression +$zipFile->addFile($file, $entryName, \PhpZip\Constants\ZipCompressionMethod::BZIP2); // BZIP2 compression +``` + +**ZipFile::addSplFile"** - adds a `\SplFileInfo` to a ZIP archive. +```php +// $file = '...../file.ext'; +// $entryName = 'file2.ext' +$zipFile = new \PhpZip\ZipFile(); + +$splFile = new \SplFileInfo('README.md'); + +$zipFile->addSplFile($splFile); +$zipFile->addSplFile($splFile, $entryName); // or $zipFile[$entryName] = new \SplFileInfo($file); -// 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 +// set compression method +$zipFile->addSplFile($splFile, $entryName, $options = [ + \PhpZip\Constants\ZipOptions::COMPRESSION_METHOD => \PhpZip\Constants\ZipCompressionMethod::DEFLATED, +]); +``` + +**ZipFile::addFromFinder"** - adds files from the `Symfony\Component\Finder\Finder` to a ZIP archive. +https://symfony.com/doc/current/components/finder.html +```php +$finder = new \Symfony\Component\Finder\Finder(); +$finder + ->files() + ->name('*.{jpg,jpeg,gif,png}') + ->name('/^[0-9a-f]\./') + ->contains('/lorem\s+ipsum$/i') + ->in('path'); + +$zipFile = new \PhpZip\ZipFile(); +$zipFile->addFromFinder($finder, $options = [ + \PhpZip\Constants\ZipOptions::COMPRESSION_METHOD => \PhpZip\Constants\ZipCompressionMethod::DEFLATED, + \PhpZip\Constants\ZipOptions::MODIFIED_TIME => new \DateTimeImmutable('-1 day 5 min') +]); ``` **ZipFile::addFromString** - adds a file to a ZIP archive using its contents. ```php +$zipFile = new \PhpZip\ZipFile(); + $zipFile[$entryName] = $contents; // or $zipFile->addFromString($entryName, $contents); // 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 +$zipFile->addFromString($entryName, $contents, \PhpZip\Constants\ZipCompressionMethod::STORED); // No compression +$zipFile->addFromString($entryName, $contents, \PhpZip\Constants\ZipCompressionMethod::DEFLATED); // Deflate compression +$zipFile->addFromString($entryName, $contents, \PhpZip\Constants\ZipCompressionMethod::BZIP2); // BZIP2 compression ``` **ZipFile::addFromStream** - adds a entry from the stream to the ZIP archive. ```php +$zipFile = new \PhpZip\ZipFile(); // $stream = fopen(..., 'rb'); $zipFile->addFromStream($stream, $entryName); @@ -394,14 +385,14 @@ $zipFile->addFromStream($stream, $entryName); $zipFile[$entryName] = $stream; // 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 +$zipFile->addFromStream($stream, $entryName, \PhpZip\Constants\ZipCompressionMethod::STORED); // No compression +$zipFile->addFromStream($stream, $entryName, \PhpZip\Constants\ZipCompressionMethod::DEFLATED); // Deflate compression +$zipFile->addFromStream($stream, $entryName, \PhpZip\Constants\ZipCompressionMethod::BZIP2); // BZIP2 compression ``` **ZipFile::addEmptyDir** - add a new directory. ```php +$zipFile = new \PhpZip\ZipFile(); // $path = "path/to/"; - $zipFile->addEmptyDir($path); // or $zipFile[$path] = null; @@ -411,67 +402,71 @@ $zipFile[$path] = 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.jpg' => fopen('..../filename', 'rb'), // add an entry from the stream 'path/to/file.dat' => new \SplFileInfo('..../filename'), // add an entry from the file ]; +$zipFile = new \PhpZip\ZipFile(); $zipFile->addAll($entries); ``` **ZipFile::addDir** - adds files to the archive from the directory on the specified path without subdirectories. ```php +$zipFile = new \PhpZip\ZipFile(); $zipFile->addDir($dirName); // you can specify the path in the archive to which you want to put entries -$localPath = "to/path/"; +$localPath = 'to/path/'; $zipFile->addDir($dirName, $localPath); // 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 +$zipFile->addDir($dirName, $localPath, \PhpZip\Constants\ZipCompressionMethod::STORED); // No compression +$zipFile->addDir($dirName, $localPath, \PhpZip\Constants\ZipCompressionMethod::DEFLATED); // Deflate compression +$zipFile->addDir($dirName, $localPath, \PhpZip\Constants\ZipCompressionMethod::BZIP2); // BZIP2 compression ``` **ZipFile::addDirRecursive** - adds files to the archive from the directory on the specified path with subdirectories. ```php +$zipFile = new \PhpZip\ZipFile(); $zipFile->addDirRecursive($dirName); // you can specify the path in the archive to which you want to put entries -$localPath = "to/path/"; +$localPath = 'to/path/'; $zipFile->addDirRecursive($dirName, $localPath); // 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 +$zipFile->addDirRecursive($dirName, $localPath, \PhpZip\Constants\ZipCompressionMethod::STORED); // No compression +$zipFile->addDirRecursive($dirName, $localPath, \PhpZip\Constants\ZipCompressionMethod::DEFLATED); // Deflate compression +$zipFile->addDirRecursive($dirName, $localPath, \PhpZip\Constants\ZipCompressionMethod::BZIP2); // BZIP2 compression ``` **ZipFile::addFilesFromIterator** - adds files from the iterator of directories. ```php // $directoryIterator = new \DirectoryIterator($dir); // without subdirectories // $directoryIterator = new \RecursiveDirectoryIterator($dir); // with subdirectories - +$zipFile = new \PhpZip\ZipFile(); $zipFile->addFilesFromIterator($directoryIterator); // you can specify the path in the archive to which you want to put entries -$localPath = "to/path/"; +$localPath = 'to/path/'; $zipFile->addFilesFromIterator($directoryIterator, $localPath); // or $zipFile[$localPath] = $directoryIterator; // 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 +$zipFile->addFilesFromIterator($directoryIterator, $localPath, \PhpZip\Constants\ZipCompressionMethod::STORED); // No compression +$zipFile->addFilesFromIterator($directoryIterator, $localPath, \PhpZip\Constants\ZipCompressionMethod::DEFLATED); // Deflate compression +$zipFile->addFilesFromIterator($directoryIterator, $localPath, \PhpZip\Constants\ZipCompressionMethod::BZIP2); // BZIP2 compression ``` Example with some files ignoring: ```php $ignoreFiles = [ - "file_ignore.txt", - "dir_ignore/sub dir ignore/" + 'file_ignore.txt', + 'dir_ignore/sub dir ignore/' ]; // $directoryIterator = new \DirectoryIterator($dir); // without subdirectories // $directoryIterator = new \RecursiveDirectoryIterator($dir); // with subdirectories - // use \PhpZip\Util\Iterator\IgnoreFilesFilterIterator for non-recursive search + +$zipFile = new \PhpZip\ZipFile(); $ignoreIterator = new \PhpZip\Util\Iterator\IgnoreFilesRecursiveFilterIterator( $directoryIterator, $ignoreFiles @@ -483,123 +478,138 @@ $zipFile->addFilesFromIterator($ignoreIterator); ```php $globPattern = '**.{jpg,jpeg,png,gif}'; // example glob pattern -> add all .jpg, .jpeg, .png and .gif files +$zipFile = new \PhpZip\ZipFile(); $zipFile->addFilesFromGlob($dir, $globPattern); // you can specify the path in the archive to which you want to put entries -$localPath = "to/path/"; +$localPath = 'to/path/'; $zipFile->addFilesFromGlob($dir, $globPattern, $localPath); // 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 +$zipFile->addFilesFromGlob($dir, $globPattern, $localPath, \PhpZip\Constants\ZipCompressionMethod::STORED); // No compression +$zipFile->addFilesFromGlob($dir, $globPattern, $localPath, \PhpZip\Constants\ZipCompressionMethod::DEFLATED); // Deflate compression +$zipFile->addFilesFromGlob($dir, $globPattern, $localPath, \PhpZip\Constants\ZipCompressionMethod::BZIP2); // BZIP2 compression ``` **ZipFile::addFilesFromGlobRecursive** - adds files from a directory by [glob pattern](https://en.wikipedia.org/wiki/Glob_(programming)) with subdirectories. ```php $globPattern = '**.{jpg,jpeg,png,gif}'; // example glob pattern -> add all .jpg, .jpeg, .png and .gif files +$zipFile = new \PhpZip\ZipFile(); $zipFile->addFilesFromGlobRecursive($dir, $globPattern); // you can specify the path in the archive to which you want to put entries -$localPath = "to/path/"; +$localPath = 'to/path/'; $zipFile->addFilesFromGlobRecursive($dir, $globPattern, $localPath); // 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 +$zipFile->addFilesFromGlobRecursive($dir, $globPattern, $localPath, \PhpZip\Constants\ZipCompressionMethod::STORED); // No compression +$zipFile->addFilesFromGlobRecursive($dir, $globPattern, $localPath, \PhpZip\Constants\ZipCompressionMethod::DEFLATED); // Deflate compression +$zipFile->addFilesFromGlobRecursive($dir, $globPattern, $localPath, \PhpZip\Constants\ZipCompressionMethod::BZIP2); // BZIP2 compression ``` **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 = new \PhpZip\ZipFile(); $zipFile->addFilesFromRegex($dir, $regexPattern); // you can specify the path in the archive to which you want to put entries -$localPath = "to/path/"; +$localPath = 'to/path/'; $zipFile->addFilesFromRegex($dir, $regexPattern, $localPath); // 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 +$zipFile->addFilesFromRegex($dir, $regexPattern, $localPath, \PhpZip\Constants\ZipCompressionMethod::STORED); // No compression +$zipFile->addFilesFromRegex($dir, $regexPattern, $localPath, \PhpZip\Constants\ZipCompressionMethod::DEFLATED); // Deflate compression +$zipFile->addFilesFromRegex($dir, $regexPattern, $localPath, \PhpZip\Constants\ZipCompressionMethod::BZIP2); // BZIP2 compression ``` **ZipFile::addFilesFromRegexRecursive** - adds files from a directory by [PCRE pattern](https://en.wikipedia.org/wiki/Regular_expression) with subdirectories. ```php $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/"; +$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 +$zipFile->addFilesFromRegexRecursive($dir, $regexPattern, $localPath, \PhpZip\Constants\ZipCompressionMethod::STORED); // No compression +$zipFile->addFilesFromRegexRecursive($dir, $regexPattern, $localPath, \PhpZip\Constants\ZipCompressionMethod::DEFLATED); // Deflate compression +$zipFile->addFilesFromRegexRecursive($dir, $regexPattern, $localPath, \PhpZip\Constants\ZipCompressionMethod::BZIP2); // BZIP2 compression ``` #### Deleting entries from the archive **ZipFile::deleteFromName** - deletes an entry in the archive using its name. ```php +$zipFile = new \PhpZip\ZipFile(); $zipFile->deleteFromName($entryName); ``` **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 = new \PhpZip\ZipFile(); $zipFile->deleteFromGlob($globPattern); ``` **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 = new \PhpZip\ZipFile(); $zipFile->deleteFromRegex($regexPattern); ``` **ZipFile::deleteAll** - deletes all entries in the ZIP archive. ```php +$zipFile = new \PhpZip\ZipFile(); $zipFile->deleteAll(); ``` #### Working with entries and archive **ZipFile::rename** - renames an entry defined by its name. ```php +$zipFile = new \PhpZip\ZipFile(); $zipFile->rename($oldName, $newName); ``` **ZipFile::setCompressionLevel** - set the compression level for all files in the archive. > _Note that this method does not apply to entries that are added after this method is run._ -By default, the compression level is -1 (`\PhpZip\ZipFile::LEVEL_DEFAULT_COMPRESSION`) or the compression level specified in the archive for Deflate compression. +By default, the compression level is 5 (`\PhpZip\Constants\ZipCompressionLevel::NORMAL`) 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. +The values range from 1 (`\PhpZip\Constants\ZipCompressionLevel::SUPER_FAST`) to 9 (`\PhpZip\Constants\ZipCompressionLevel::MAXIMUM`) are supported. The higher the number, the better and longer the compression. ```php -$zipFile->setCompressionLevel(\PhpZip\ZipFile::LEVEL_BEST_COMPRESSION); +$zipFile = new \PhpZip\ZipFile(); +$zipFile->setCompressionLevel(\PhpZip\Constants\ZipCompressionLevel::MAXIMUM); ``` **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. +The values range from 1 (`\PhpZip\Constants\ZipCompressionLevel::SUPER_FAST`) to 9 (`\PhpZip\Constants\ZipCompressionLevel::MAXIMUM`) are supported. The higher the number, the better and longer the compression. ```php -$zipFile->setCompressionLevelEntry($entryName, \PhpZip\ZipFile::LEVEL_BEST_COMPRESSION); +$zipFile = new \PhpZip\ZipFile(); +$zipFile->setCompressionLevelEntry($entryName, \PhpZip\Constants\ZipCompressionLevel::FAST); ``` **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` +- `\PhpZip\Constants\ZipCompressionMethod::STORED` - No compression +- `\PhpZip\Constants\ZipCompressionMethod::DEFLATED` - Deflate compression +- `\PhpZip\Constants\ZipCompressionMethod::BZIP2` - Bzip2 compression with the extension `ext-bz2` ```php -$zipFile->setCompressionMethodEntry($entryName, ZipFile::METHOD_DEFLATED); +$zipFile = new \PhpZip\ZipFile(); +$zipFile->setCompressionMethodEntry($entryName, \PhpZip\Constants\ZipCompressionMethod::DEFLATED); ``` **ZipFile::setArchiveComment** - set the comment of a ZIP archive. ```php +$zipFile = new \PhpZip\ZipFile(); $zipFile->setArchiveComment($commentArchive); ``` **ZipFile::setEntryComment** - set the comment of an entry defined by its name. ```php +$zipFile = new \PhpZip\ZipFile(); $zipFile->setEntryComment($entryName, $comment); ``` **ZipFile::matcher** - selecting entries in the archive to perform operations on them. ```php +$zipFile = new \PhpZip\ZipFile(); $matcher = $zipFile->matcher(); ``` Selecting files from the archive one at a time: @@ -638,7 +648,7 @@ $entries = $matcher->getMatches(); invoke() - invoke a callable function on selected entries: ```php // example -$matcher->invoke(function($entryName) use($zipFile) { +$matcher->invoke(static function($entryName) use($zipFile) { $newName = preg_replace('~\.(jpe?g)$~i', '.no_optimize.$1', $entryName); $zipFile->rename($entryName, $newName); }); @@ -654,10 +664,10 @@ $matcher->disableEncryption(); // disables encryption for selected entries #### Working with passwords Implemented support for encryption methods: -- `\PhpZip\ZipFile::ENCRYPTION_METHOD_TRADITIONAL` - Traditional PKWARE encryption -- `\PhpZip\ZipFile::ENCRYPTION_METHOD_WINZIP_AES_256` - WinZip AES encryption 256 bit (recommended) -- `\PhpZip\ZipFile::ENCRYPTION_METHOD_WINZIP_AES_192` - WinZip AES encryption 192 bit -- `\PhpZip\ZipFile::ENCRYPTION_METHOD_WINZIP_AES_128` - WinZip AES encryption 128 bit +- `\PhpZip\Constants\ZipEncryptionMethod::PKWARE` - Traditional PKWARE encryption (legacy) +- `\PhpZip\Constants\ZipEncryptionMethod::WINZIP_AES_256` - WinZip AES encryption 256 bit (recommended) +- `\PhpZip\Constants\ZipEncryptionMethod::WINZIP_AES_192` - WinZip AES encryption 192 bit +- `\PhpZip\Constants\ZipEncryptionMethod::WINZIP_AES_128` - WinZip AES encryption 128 bit **ZipFile::setReadPassword** - set the password for the open archive. @@ -677,7 +687,7 @@ $zipFile->setPassword($password); ``` You can set the encryption method: ```php -$encryptionMethod = ZipFile::ENCRYPTION_METHOD_WINZIP_AES_256; +$encryptionMethod = \PhpZip\Constants\ZipEncryptionMethod::WINZIP_AES_256; $zipFile->setPassword($password, $encryptionMethod); ``` **ZipFile::setPasswordEntry** - sets a new password of an entry defined by its name. @@ -686,7 +696,7 @@ $zipFile->setPasswordEntry($entryName, $password); ``` You can set the encryption method: ```php -$encryptionMethod = ZipFile::ENCRYPTION_METHOD_WINZIP_AES_256; +$encryptionMethod = \PhpZip\Constants\ZipEncryptionMethod::WINZIP_AES_256; $zipFile->setPasswordEntry($entryName, $password, $encryptionMethod); ``` **ZipFile::disableEncryption** - disable encryption for all entries that are already in the archive. @@ -744,7 +754,7 @@ $zipFile->outputAsAttachment($outputFilename); ``` You can set the Mime-Type: ```php -$mimeType = 'application/zip' +$mimeType = 'application/zip'; $zipFile->outputAsAttachment($outputFilename, $mimeType); ``` **ZipFile::outputAsResponse** - outputs a ZIP-archive as [PSR-7 Response](http://www.php-fig.org/psr/psr-7/). @@ -756,19 +766,9 @@ $zipFile->outputAsResponse($response, $outputFilename); ``` You can set the Mime-Type: ```php -$mimeType = 'application/zip' +$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(); @@ -785,10 +785,10 @@ composer install --dev ``` Run the tests: ```bash -vendor/bin/phpunit -v -c phpunit.xml +vendor/bin/phpunit ``` ### Changelog -[Link to Changelog](CHANGELOG.md) +Changes are documented in the [releases page](https://github.com/Ne-Lexa/php-zip/releases). ### Upgrade #### Upgrade version 2 to version 3.0 diff --git a/composer.json b/composer.json index b0a938f..92ec96b 100644 --- a/composer.json +++ b/composer.json @@ -21,23 +21,28 @@ } ], "require": { - "php": "^5.5 || ^7.0", + "php": "^5.5.9 || ^7.0", "ext-zlib": "*", "psr/http-message": "^1.0", - "paragonie/random_compat": ">=1 <9.99" + "paragonie/random_compat": ">=1 <9.99", + "symfony/finder": "^3.0|^4.0|^5.0" }, "require-dev": { + "ext-bz2": "*", + "ext-openssl": "*", + "ext-fileinfo": "*", "phpunit/phpunit": "^4.8|^5.7", - "zendframework/zend-diactoros": "^1.4" + "zendframework/zend-diactoros": "^1.4", + "symfony/var-dumper": "^3.0|^4.0|^5.0" }, "autoload": { "psr-4": { - "PhpZip\\": "src/PhpZip" + "PhpZip\\": "src/" } }, "autoload-dev": { "psr-4": { - "PhpZip\\": "tests/PhpZip" + "PhpZip\\Tests\\": "tests/" } }, "suggest": { diff --git a/phpunit.xml b/phpunit.xml index c69aee3..1633bee 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -10,9 +10,16 @@ - + tests + + tests + tests/SlowTests + + + tests/SlowTests + @@ -20,4 +27,4 @@ src - \ No newline at end of file + diff --git a/src/Constants/DosAttrs.php b/src/Constants/DosAttrs.php new file mode 100644 index 0000000..5bebe65 --- /dev/null +++ b/src/Constants/DosAttrs.php @@ -0,0 +1,33 @@ + 'Stored', + 1 => 'Shrunk', + 2 => 'Reduced compression factor 1', + 3 => 'Reduced compression factor 2', + 4 => 'Reduced compression factor 3', + 5 => 'Reduced compression factor 4', + 6 => 'Imploded', + 7 => 'Reserved for Tokenizing compression algorithm', + self::DEFLATED => 'Deflated', + 9 => 'Enhanced Deflating using Deflate64(tm)', + 10 => 'PKWARE Data Compression Library Imploding', + 11 => 'Reserved by PKWARE', + self::BZIP2 => 'BZIP2', + 13 => 'Reserved by PKWARE', + 14 => 'LZMA', + 15 => 'Reserved by PKWARE', + 16 => 'Reserved by PKWARE', + 17 => 'Reserved by PKWARE', + 18 => 'File is compressed using IBM TERSE (new)', + 19 => 'IBM LZ77 z Architecture (PFS)', + 96 => 'WinZip JPEG Compression', + 97 => 'WavPack compressed data', + 98 => 'PPMd version I, Rev 1', + self::WINZIP_AES => 'AES Encryption', + ]; + + /** + * @param int $value + * + * @return string + */ + public static function getCompressionMethodName($value) + { + return isset(self::$ZIP_COMPRESSION_METHODS[$value]) ? + self::$ZIP_COMPRESSION_METHODS[$value] : + 'Unknown Method'; + } + + /** + * @return int[] + */ + public static function getSupportMethods() + { + static $methods; + + if ($methods === null) { + $methods = [ + self::STORED, + self::DEFLATED, + ]; + + if (\extension_loaded('bz2')) { + $methods[] = self::BZIP2; + } + } + + return $methods; + } + + /** + * @param int $compressionMethod + * + * @throws ZipUnsupportMethodException + */ + public static function checkSupport($compressionMethod) + { + $compressionMethod = (int) $compressionMethod; + + if (!\in_array($compressionMethod, self::getSupportMethods(), true)) { + throw new ZipUnsupportMethodException(sprintf( + 'Compression method %d (%s) is not supported.', + $compressionMethod, + self::getCompressionMethodName($compressionMethod) + )); + } + } +} diff --git a/src/Constants/ZipConstants.php b/src/Constants/ZipConstants.php new file mode 100644 index 0000000..39fca0f --- /dev/null +++ b/src/Constants/ZipConstants.php @@ -0,0 +1,99 @@ + */ + private static $ENCRYPTION_METHODS = [ + self::NONE => 'no encryption', + self::PKWARE => 'Traditional PKWARE encryption', + self::WINZIP_AES_128 => 'WinZip AES-128', + self::WINZIP_AES_192 => 'WinZip AES-192', + self::WINZIP_AES_256 => 'WinZip AES-256', + ]; + + /** + * @param int $value + * + * @return string + */ + public static function getEncryptionMethodName($value) + { + $value = (int) $value; + + return isset(self::$ENCRYPTION_METHODS[$value]) ? + self::$ENCRYPTION_METHODS[$value] : + 'Unknown Encryption Method'; + } + + /** + * @param int $encryptionMethod + * + * @return bool + */ + public static function hasEncryptionMethod($encryptionMethod) + { + return isset(self::$ENCRYPTION_METHODS[$encryptionMethod]); + } + + /** + * @param int $encryptionMethod + * + * @return bool + */ + public static function isWinZipAesMethod($encryptionMethod) + { + return \in_array( + (int) $encryptionMethod, + [ + self::WINZIP_AES_256, + self::WINZIP_AES_192, + self::WINZIP_AES_128, + ], + true + ); + } + + /** + * @param int $encryptionMethod + * + * @throws InvalidArgumentException + */ + public static function checkSupport($encryptionMethod) + { + $encryptionMethod = (int) $encryptionMethod; + + if (!self::hasEncryptionMethod($encryptionMethod)) { + throw new InvalidArgumentException(sprintf( + 'Encryption method %d is not supported.', + $encryptionMethod + )); + } + } +} diff --git a/src/Constants/ZipOptions.php b/src/Constants/ZipOptions.php new file mode 100644 index 0000000..25c88a7 --- /dev/null +++ b/src/Constants/ZipOptions.php @@ -0,0 +1,29 @@ + 'MS-DOS', + 1 => 'Amiga', + 2 => 'OpenVMS', + self::OS_UNIX => 'Unix', + 4 => 'VM/CMS', + 5 => 'Atari ST', + 6 => 'HPFS (OS/2, NT 3.x)', + 7 => 'Macintosh', + 8 => 'Z-System', + 9 => 'CP/M', + 10 => 'Windows NTFS or TOPS-20', + 11 => 'MVS or NTFS', + 12 => 'VSE or SMS/QDOS', + 13 => 'Acorn RISC OS', + 14 => 'VFAT', + 15 => 'alternate MVS', + 16 => 'BeOS', + 17 => 'Tandem', + 18 => 'OS/400', + self::OS_MAC_OSX => 'OS/X (Darwin)', + 30 => 'AtheOS/Syllable', + ]; + + /** + * @param int $platform + * + * @return string + */ + public static function getPlatformName($platform) + { + return isset(self::$platforms[$platform]) ? self::$platforms[$platform] : 'Unknown'; + } +} diff --git a/src/Constants/ZipVersion.php b/src/Constants/ZipVersion.php new file mode 100644 index 0000000..58c090d --- /dev/null +++ b/src/Constants/ZipVersion.php @@ -0,0 +1,81 @@ +getName() : $entryName; + parent::__construct(sprintf( + 'Zip Entry "%s" was not found in the archive.', + $entryName + )); $this->entryName = $entryName; } diff --git a/src/PhpZip/Exception/ZipException.php b/src/Exception/ZipException.php similarity index 100% rename from src/PhpZip/Exception/ZipException.php rename to src/Exception/ZipException.php diff --git a/src/PhpZip/Exception/ZipUnsupportMethodException.php b/src/Exception/ZipUnsupportMethodException.php similarity index 59% rename from src/PhpZip/Exception/ZipUnsupportMethodException.php rename to src/Exception/ZipUnsupportMethodException.php index 2f03167..027fa3b 100644 --- a/src/PhpZip/Exception/ZipUnsupportMethodException.php +++ b/src/Exception/ZipUnsupportMethodException.php @@ -5,6 +5,6 @@ namespace PhpZip\Exception; /** * Class ZipUnsupportMethodException. */ -class ZipUnsupportMethodException extends RuntimeException +class ZipUnsupportMethodException extends ZipException { } diff --git a/src/PhpZip/Crypto/TraditionalPkwareEncryptionEngine.php b/src/IO/Filter/Cipher/Traditional/PKCryptContext.php similarity index 70% rename from src/PhpZip/Crypto/TraditionalPkwareEncryptionEngine.php rename to src/IO/Filter/Cipher/Traditional/PKCryptContext.php index 1d12603..ad42457 100644 --- a/src/PhpZip/Crypto/TraditionalPkwareEncryptionEngine.php +++ b/src/IO/Filter/Cipher/Traditional/PKCryptContext.php @@ -1,11 +1,9 @@ entry = $entry; - } - - /** - * Initial keys. + * PKCryptContext constructor. * * @param string $password */ - private function initKeys($password) + public function __construct($password) { - $this->keys[0] = 305419896; - $this->keys[1] = 591751049; - $this->keys[2] = 878082192; + if (\PHP_INT_SIZE === 4) { + throw new RuntimeException('Traditional PKWARE Encryption is not supported in 32-bit PHP.'); + } + + $this->keys = [ + 305419896, + 591751049, + 878082192, + ]; foreach (unpack('C*', $password) as $b) { $this->updateKeys($b); } } + /** + * @param string $header + * @param int $checkByte + * + * @throws ZipAuthenticationException + */ + public function checkHeader($header, $checkByte) + { + $byte = 0; + + foreach (unpack('C*', $header) as $byte) { + $byte = ($byte ^ $this->decryptByte()) & 0xff; + $this->updateKeys($byte); + } + + if ($byte !== $checkByte) { + throw new ZipAuthenticationException(sprintf('Invalid password')); + } + } + + /** + * @param string $content + * + * @return string + */ + public function decryptString($content) + { + $decryptContent = ''; + + foreach (unpack('C*', $content) as $byte) { + $byte = ($byte ^ $this->decryptByte()) & 0xff; + $this->updateKeys($byte); + $decryptContent .= \chr($byte); + } + + return $decryptContent; + } + + /** + * Decrypt byte. + * + * @return int + */ + private function decryptByte() + { + $temp = $this->keys[2] | 2; + + return (($temp * ($temp ^ 1)) >> 8) & 0xffffff; + } + /** * Update keys. * @@ -350,123 +391,17 @@ class TraditionalPkwareEncryptionEngine implements ZipEncryptionEngine /** * @param string $content * - * @throws ZipAuthenticationException - * * @return string */ - public function decrypt($content) + public function encryptString($content) { - if (\PHP_INT_SIZE === 4) { - throw new RuntimeException('Traditional PKWARE Encryption is not supported in 32-bit PHP.'); - } - - $password = $this->entry->getPassword(); - $this->initKeys($password); - - $headerBytes = array_values(unpack('C*', substr($content, 0, self::STD_DEC_HDR_SIZE))); - $byte = 0; - - for ($i = 0; $i < self::STD_DEC_HDR_SIZE; $i++) { - $byte = ($headerBytes[$i] ^ $this->decryptByte()) & 0xff; - $this->updateKeys($byte); - } - - if ($this->entry->getGeneralPurposeBitFlag(ZipEntry::GPBF_DATA_DESCRIPTOR)) { - // compare against the file type from extended local headers - $checkByte = ($this->entry->getDosTime() >> 8) & 0xff; - } else { - // compare against the CRC otherwise - $checkByte = ($this->entry->getCrc() >> 24) & 0xff; - } - - if ($byte !== $checkByte) { - throw new ZipAuthenticationException( - sprintf( - 'Invalid password for zip entry "%s"', - $this->entry->getName() - ) - ); - } - - $outputContent = ''; - - foreach (unpack('C*', substr($content, self::STD_DEC_HDR_SIZE)) as $val) { - $val = ($val ^ $this->decryptByte()) & 0xff; - $this->updateKeys($val); - $outputContent .= pack('c', $val); - } - - return $outputContent; - } - - /** - * Decrypt byte. - * - * @return int - */ - private function decryptByte() - { - $temp = $this->keys[2] | 2; - - return (($temp * ($temp ^ 1)) >> 8) & 0xffffff; - } - - /** - * Encryption data. - * - * @param string $data - * - * @throws ZipCryptoException - * - * @return string - */ - public function encrypt($data) - { - if (\PHP_INT_SIZE === 4) { - throw new RuntimeException('Traditional PKWARE Encryption is not supported in 32-bit PHP.'); - } - - $crc = $this->entry->isDataDescriptorRequired() ? - ($this->entry->getDosTime() & 0x0000ffff) << 16 : - $this->entry->getCrc(); - - try { - $headerBytes = random_bytes(self::STD_DEC_HDR_SIZE); - } catch (\Exception $e) { - throw new \RuntimeException('Oops, our server is bust and cannot generate any random data.', 1, $e); - } - - // Initialize again since the generated bytes were encrypted. - $password = $this->entry->getPassword(); - $this->initKeys($password); - - $headerBytes[self::STD_DEC_HDR_SIZE - 1] = pack('c', ($crc >> 24) & 0xff); - $headerBytes[self::STD_DEC_HDR_SIZE - 2] = pack('c', ($crc >> 16) & 0xff); - - $headerBytes = $this->encryptData($headerBytes); - - return $headerBytes . $this->encryptData($data); - } - - /** - * @param string $content - * - * @throws ZipCryptoException - * - * @return string - */ - private function encryptData($content) - { - if ($content === null) { - throw new ZipCryptoException('content is null'); - } - $buff = ''; + $encryptContent = ''; foreach (unpack('C*', $content) as $val) { - $buff .= pack('c', $this->encryptByte($val)); + $encryptContent .= pack('c', $this->encryptByte($val)); } - return $buff; + return $encryptContent; } /** diff --git a/src/IO/Filter/Cipher/Traditional/PKDecryptionStreamFilter.php b/src/IO/Filter/Cipher/Traditional/PKDecryptionStreamFilter.php new file mode 100644 index 0000000..0b765f8 --- /dev/null +++ b/src/IO/Filter/Cipher/Traditional/PKDecryptionStreamFilter.php @@ -0,0 +1,118 @@ +params['entry'])) { + return false; + } + + if (!($this->params['entry'] instanceof ZipEntry)) { + throw new \RuntimeException('ZipEntry expected'); + } + /** @var ZipEntry $entry */ + $entry = $this->params['entry']; + $password = $entry->getPassword(); + + if ($password === null) { + return false; + } + + $this->size = $entry->getCompressedSize(); + + // init context + $this->context = new PKCryptContext($password); + + // init check byte + if ($entry->isDataDescriptorEnabled()) { + $this->checkByte = ($entry->getDosTime() >> 8) & 0xff; + } else { + $this->checkByte = ($entry->getCrc() >> 24) & 0xff; + } + + $this->readLength = 0; + $this->readHeader = false; + + return true; + } + + /** + * Decryption filter. + * + * @param resource $in + * @param resource $out + * @param int $consumed + * @param bool $closing + * + * @throws ZipException + * + * @return int + * + * @todo USE FFI in php 7.4 + */ + public function filter($in, $out, &$consumed, $closing) + { + while ($bucket = stream_bucket_make_writeable($in)) { + $buffer = $bucket->data; + $this->readLength += $bucket->datalen; + + if ($this->readLength > $this->size) { + $buffer = substr($buffer, 0, $this->size - $this->readLength); + } + + if (!$this->readHeader) { + $header = substr($buffer, 0, PKCryptContext::STD_DEC_HDR_SIZE); + $this->context->checkHeader($header, $this->checkByte); + + $buffer = substr($buffer, PKCryptContext::STD_DEC_HDR_SIZE); + $this->readHeader = true; + } + + $bucket->data = $this->context->decryptString($buffer); + + $consumed += $bucket->datalen; + stream_bucket_append($out, $bucket); + } + + return \PSFS_PASS_ON; + } +} diff --git a/src/IO/Filter/Cipher/Traditional/PKEncryptionStreamFilter.php b/src/IO/Filter/Cipher/Traditional/PKEncryptionStreamFilter.php new file mode 100644 index 0000000..8176bd2 --- /dev/null +++ b/src/IO/Filter/Cipher/Traditional/PKEncryptionStreamFilter.php @@ -0,0 +1,128 @@ +params['entry'], $this->params['size'])) { + return false; + } + + if (!($this->params['entry'] instanceof ZipEntry)) { + throw new \RuntimeException('ZipEntry expected'); + } + /** @var ZipEntry $entry */ + $entry = $this->params['entry']; + $password = $entry->getPassword(); + + if ($password === null) { + return false; + } + + $this->size = (int) $this->params['size']; + + // init keys + $this->context = new PKCryptContext($password); + + $crc = $entry->isDataDescriptorRequired() ? + ($entry->getDosTime() & 0x0000ffff) << 16 : + $entry->getCrc(); + + try { + $headerBytes = random_bytes(PKCryptContext::STD_DEC_HDR_SIZE); + } catch (\Exception $e) { + throw new \RuntimeException('Oops, our server is bust and cannot generate any random data.', 1, $e); + } + + $headerBytes[PKCryptContext::STD_DEC_HDR_SIZE - 1] = pack('c', ($crc >> 24) & 0xff); + $headerBytes[PKCryptContext::STD_DEC_HDR_SIZE - 2] = pack('c', ($crc >> 16) & 0xff); + + $this->headerBytes = $headerBytes; + $this->writeLength = 0; + $this->writeHeader = false; + + return true; + } + + /** + * Encryption filter. + * + * @param resource $in + * @param resource $out + * @param int $consumed + * @param bool $closing + * + * @return int + * + * @todo USE FFI in php 7.4 + */ + public function filter($in, $out, &$consumed, $closing) + { + while ($bucket = stream_bucket_make_writeable($in)) { + $buffer = $bucket->data; + $this->writeLength += $bucket->datalen; + + if ($this->writeLength > $this->size) { + $buffer = substr($buffer, 0, $this->size - $this->writeLength); + } + + $data = ''; + + if (!$this->writeHeader) { + $data .= $this->context->encryptString($this->headerBytes); + $this->writeHeader = true; + } + + $data .= $this->context->encryptString($buffer); + + $bucket->data = $data; + + $consumed += $bucket->datalen; + stream_bucket_append($out, $bucket); + } + + return \PSFS_PASS_ON; + } +} diff --git a/src/IO/Filter/Cipher/WinZipAes/WinZipAesContext.php b/src/IO/Filter/Cipher/WinZipAes/WinZipAesContext.php new file mode 100644 index 0000000..2691160 --- /dev/null +++ b/src/IO/Filter/Cipher/WinZipAes/WinZipAesContext.php @@ -0,0 +1,166 @@ +iv = str_repeat("\0", self::IV_SIZE); + $keyStrengthBytes = (int) ($encryptionStrengthBits / 8); + $hashLength = $keyStrengthBytes * 2 + self::PASSWORD_VERIFIER_SIZE * 8; + + $hash = hash_pbkdf2( + 'sha1', + $password, + $salt, + self::ITERATION_COUNT, + $hashLength, + true + ); + + $this->key = substr($hash, 0, $keyStrengthBytes); + $sha1Mac = substr($hash, $keyStrengthBytes, $keyStrengthBytes); + $this->hmacContext = hash_init('sha1', \HASH_HMAC, $sha1Mac); + $this->passwordVerifier = substr($hash, 2 * $keyStrengthBytes, self::PASSWORD_VERIFIER_SIZE); + } + + /** + * @return string + */ + public function getPasswordVerifier() + { + return $this->passwordVerifier; + } + + public function updateIv() + { + for ($ivCharIndex = 0; $ivCharIndex < self::IV_SIZE; $ivCharIndex++) { + $ivByte = \ord($this->iv[$ivCharIndex]); + + if (++$ivByte === 256) { + // overflow, set this one to 0, increment next + $this->iv[$ivCharIndex] = "\0"; + } else { + // no overflow, just write incremented number back and abort + $this->iv[$ivCharIndex] = \chr($ivByte); + + break; + } + } + } + + /** + * @param string $data + * + * @return string + */ + public function decryption($data) + { + hash_update($this->hmacContext, $data); + + return CryptoUtil::decryptAesCtr($data, $this->key, $this->iv); + } + + /** + * @param string $data + * + * @return string + */ + public function encrypt($data) + { + $encryptionData = CryptoUtil::encryptAesCtr($data, $this->key, $this->iv); + hash_update($this->hmacContext, $encryptionData); + + return $encryptionData; + } + + /** + * @param string $authCode + * + * @throws ZipAuthenticationException + */ + public function checkAuthCode($authCode) + { + $hmac = $this->getHmac(); + + // check authenticationCode + if (strcmp($hmac, $authCode) !== 0) { + throw new ZipAuthenticationException('Authenticated WinZip AES entry content has been tampered with.'); + } + } + + /** + * @return string + */ + public function getHmac() + { + return substr( + hash_final($this->hmacContext, true), + 0, + self::FOOTER_SIZE + ); + } +} diff --git a/src/IO/Filter/Cipher/WinZipAes/WinZipAesDecryptionStreamFilter.php b/src/IO/Filter/Cipher/WinZipAes/WinZipAesDecryptionStreamFilter.php new file mode 100644 index 0000000..7839172 --- /dev/null +++ b/src/IO/Filter/Cipher/WinZipAes/WinZipAesDecryptionStreamFilter.php @@ -0,0 +1,187 @@ +params['entry'])) { + return false; + } + + if (!($this->params['entry'] instanceof ZipEntry)) { + throw new \RuntimeException('ZipEntry expected'); + } + $this->entry = $this->params['entry']; + + if ( + $this->entry->getPassword() === null || + !$this->entry->isEncrypted() || + !$this->entry->hasExtraField(WinZipAesExtraField::HEADER_ID) + ) { + return false; + } + + $this->buffer = ''; + + return true; + } + + /** + * @param resource $in + * @param resource $out + * @param int $consumed + * @param bool $closing + * + * @throws ZipAuthenticationException + * + * @return int + */ + public function filter($in, $out, &$consumed, $closing) + { + while ($bucket = stream_bucket_make_writeable($in)) { + $this->buffer .= $bucket->data; + $this->readLength += $bucket->datalen; + + if ($this->readLength > $this->entry->getCompressedSize()) { + $this->buffer = substr($this->buffer, 0, $this->entry->getCompressedSize() - $this->readLength); + } + + // read header + if ($this->context === null) { + /** + * @var WinZipAesExtraField|null $winZipExtra + */ + $winZipExtra = $this->entry->getExtraField(WinZipAesExtraField::HEADER_ID); + + if ($winZipExtra === null) { + throw new RuntimeException('$winZipExtra is null'); + } + $saltSize = $winZipExtra->getSaltSize(); + $headerSize = $saltSize + WinZipAesContext::PASSWORD_VERIFIER_SIZE; + + if (\strlen($this->buffer) < $headerSize) { + return \PSFS_FEED_ME; + } + + $salt = substr($this->buffer, 0, $saltSize); + $passwordVerifier = substr($this->buffer, $saltSize, WinZipAesContext::PASSWORD_VERIFIER_SIZE); + $password = $this->entry->getPassword(); + + if ($password === null) { + throw new RuntimeException('$password is null'); + } + $this->context = new WinZipAesContext($winZipExtra->getEncryptionStrength(), $password, $salt); + unset($password); + + // Verify password. + if ($passwordVerifier !== $this->context->getPasswordVerifier()) { + throw new ZipAuthenticationException('Invalid password'); + } + + $this->encBlockPosition = 0; + $this->encBlockLength = $this->entry->getCompressedSize() - $headerSize - WinZipAesContext::FOOTER_SIZE; + + $this->buffer = substr($this->buffer, $headerSize); + } + + // encrypt data + $plainText = ''; + $offset = 0; + $len = \strlen($this->buffer); + $remaining = $this->encBlockLength - $this->encBlockPosition; + + if ($remaining >= WinZipAesContext::BLOCK_SIZE && $len < WinZipAesContext::BLOCK_SIZE) { + return \PSFS_FEED_ME; + } + $limit = min($len, $remaining); + + if ($remaining > $limit && ($limit % WinZipAesContext::BLOCK_SIZE) !== 0) { + $limit -= ($limit % WinZipAesContext::BLOCK_SIZE); + } + + while ($offset < $limit) { + $this->context->updateIv(); + $length = min(WinZipAesContext::BLOCK_SIZE, $limit - $offset); + $data = substr($this->buffer, 0, $length); + $plainText .= $this->context->decryption($data); + $offset += $length; + $this->buffer = substr($this->buffer, $length); + } + $this->encBlockPosition += $offset; + + if ( + $this->encBlockPosition === $this->encBlockLength && + \strlen($this->buffer) === WinZipAesContext::FOOTER_SIZE + ) { + $this->authenticationCode = $this->buffer; + $this->buffer = ''; + } + + $bucket->data = $plainText; + $consumed += $bucket->datalen; + stream_bucket_append($out, $bucket); + } + + return \PSFS_PASS_ON; + } + + /** + * @see http://php.net/manual/en/php-user-filter.onclose.php + * + * @throws ZipAuthenticationException + */ + public function onClose() + { + $this->buffer = ''; + + if ($this->context !== null) { + $this->context->checkAuthCode($this->authenticationCode); + } + } +} diff --git a/src/IO/Filter/Cipher/WinZipAes/WinZipAesEncryptionStreamFilter.php b/src/IO/Filter/Cipher/WinZipAes/WinZipAesEncryptionStreamFilter.php new file mode 100644 index 0000000..12d12fe --- /dev/null +++ b/src/IO/Filter/Cipher/WinZipAes/WinZipAesEncryptionStreamFilter.php @@ -0,0 +1,158 @@ +params['entry'])) { + return false; + } + + if (!($this->params['entry'] instanceof ZipEntry)) { + throw new \RuntimeException('ZipEntry expected'); + } + $this->entry = $this->params['entry']; + + if ( + $this->entry->getPassword() === null || + !$this->entry->isEncrypted() || + !$this->entry->hasExtraField(WinZipAesExtraField::HEADER_ID) + ) { + return false; + } + + $this->size = (int) $this->params['size']; + $this->context = null; + $this->buffer = ''; + + return true; + } + + /** + * @param resource $in + * @param resource $out + * @param int $consumed + * @param bool $closing + * + * @return int + */ + public function filter($in, $out, &$consumed, $closing) + { + while ($bucket = stream_bucket_make_writeable($in)) { + $this->buffer .= $bucket->data; + $this->remaining += $bucket->datalen; + + if ($this->remaining > $this->size) { + $this->buffer = substr($this->buffer, 0, $this->size - $this->remaining); + $this->remaining = $this->size; + } + + $encryptionText = ''; + + // write header + if ($this->context === null) { + /** + * @var WinZipAesExtraField|null $winZipExtra + */ + $winZipExtra = $this->entry->getExtraField(WinZipAesExtraField::HEADER_ID); + + if ($winZipExtra === null) { + throw new RuntimeException('$winZipExtra is null'); + } + $saltSize = $winZipExtra->getSaltSize(); + + try { + $salt = random_bytes($saltSize); + } catch (\Exception $e) { + throw new \RuntimeException('Oops, our server is bust and cannot generate any random data.', 1, $e); + } + $password = $this->entry->getPassword(); + + if ($password === null) { + throw new RuntimeException('$password is null'); + } + $this->context = new WinZipAesContext( + $winZipExtra->getEncryptionStrength(), + $password, + $salt + ); + + $encryptionText .= $salt . $this->context->getPasswordVerifier(); + } + + // encrypt data + $offset = 0; + $len = \strlen($this->buffer); + $remaining = $this->remaining - $this->size; + + if ($remaining >= WinZipAesContext::BLOCK_SIZE && $len < WinZipAesContext::BLOCK_SIZE) { + return \PSFS_FEED_ME; + } + $limit = max($len, $remaining); + + if ($remaining > $limit && ($limit % WinZipAesContext::BLOCK_SIZE) !== 0) { + $limit -= ($limit % WinZipAesContext::BLOCK_SIZE); + } + + while ($offset < $limit) { + $this->context->updateIv(); + $length = min(WinZipAesContext::BLOCK_SIZE, $limit - $offset); + $encryptionText .= $this->context->encrypt( + substr($this->buffer, 0, $length) + ); + $offset += $length; + $this->buffer = substr($this->buffer, $length); + } + + if ($remaining === 0) { + $encryptionText .= $this->context->getHmac(); + } + + $bucket->data = $encryptionText; + $consumed += $bucket->datalen; + + stream_bucket_append($out, $bucket); + } + + return \PSFS_PASS_ON; + } +} diff --git a/src/PhpZip/Stream/ResponseStream.php b/src/IO/Stream/ResponseStream.php similarity index 96% rename from src/PhpZip/Stream/ResponseStream.php rename to src/IO/Stream/ResponseStream.php index 0f37f4b..e016103 100644 --- a/src/PhpZip/Stream/ResponseStream.php +++ b/src/IO/Stream/ResponseStream.php @@ -1,6 +1,6 @@ uri) { + if ($this->uri !== null) { clearstatcache(true, $this->uri); } $stats = fstat($this->stream); @@ -326,8 +326,12 @@ class ResponseStream implements StreamInterface public function detach() { $result = $this->stream; - $this->stream = $this->size = $this->uri = null; - $this->readable = $this->writable = $this->seekable = false; + $this->stream = null; + $this->size = null; + $this->uri = null; + $this->readable = false; + $this->writable = false; + $this->seekable = false; return $result; } diff --git a/src/IO/Stream/ZipEntryStreamWrapper.php b/src/IO/Stream/ZipEntryStreamWrapper.php new file mode 100644 index 0000000..aa13f07 --- /dev/null +++ b/src/IO/Stream/ZipEntryStreamWrapper.php @@ -0,0 +1,309 @@ + [ + 'entry' => $entry, + ], + ] + ); + + $uri = self::PROTOCOL . '://' . $entry->getName(); + $fp = fopen($uri, 'r+b', false, $context); + + if ($fp === false) { + throw new \RuntimeException('Error open ' . $uri); + } + + return $fp; + } + + /** + * Opens file or URL. + * + * This method is called immediately after the wrapper is + * initialized (f.e. by {@see fopen()} and {@see file_get_contents()}). + * + * @param string $path specifies the URL that was passed to + * the original function + * @param string $mode the mode used to open the file, as detailed + * for {@see fopen()} + * @param int $options Holds additional flags set by the streams + * API. It can hold one or more of the + * following values OR'd together. + * @param string $opened_path if the path is opened successfully, and + * STREAM_USE_PATH is set in options, + * opened_path should be set to the + * full path of the file/resource that + * was actually opened + * + * @throws ZipException + * + * @return bool + * + * @see https://www.php.net/streamwrapper.stream-open + */ + public function stream_open($path, $mode, $options, &$opened_path) + { + if ($this->context === null) { + throw new \RuntimeException('stream context is null'); + } + $streamOptions = stream_context_get_options($this->context); + + if (!isset($streamOptions[self::PROTOCOL]['entry'])) { + throw new \RuntimeException('no stream option ["' . self::PROTOCOL . '"]["entry"]'); + } + $zipEntry = $streamOptions[self::PROTOCOL]['entry']; + + if (!$zipEntry instanceof ZipEntry) { + throw new \RuntimeException('invalid stream context'); + } + + $zipData = $zipEntry->getData(); + + if ($zipData === null) { + throw new ZipException(sprintf('No data for zip entry "%s"', $zipEntry->getName())); + } + $this->fp = $zipData->getDataAsStream(); + + return $this->fp !== false; + } + + /** + * Read from stream. + * + * This method is called in response to {@see fread()} and {@see fgets()}. + * + * Note: Remember to update the read/write position of the stream + * (by the number of bytes that were successfully read). + * + * @param int $count how many bytes of data from the current + * position should be returned + * + * @return false|string If there are less than count bytes available, + * return as many as are available. If no more data + * is available, return either FALSE or + * an empty string. + * + * @see https://www.php.net/streamwrapper.stream-read + */ + public function stream_read($count) + { + return fread($this->fp, $count); + } + + /** + * Seeks to specific location in a stream. + * + * This method is called in response to {@see fseek()}. + * The read/write position of the stream should be updated according + * to the offset and whence. + * + * @param int $offset the stream offset to seek to + * @param int $whence Possible values: + * {@see \SEEK_SET} - Set position equal to offset bytes. + * {@see \SEEK_CUR} - Set position to current location plus offset. + * {@see \SEEK_END} - Set position to end-of-file plus offset. + * + * @return bool return TRUE if the position was updated, FALSE otherwise + * + * @see https://www.php.net/streamwrapper.stream-seek + */ + public function stream_seek($offset, $whence = \SEEK_SET) + { + return fseek($this->fp, $offset, $whence) === 0; + } + + /** + * Retrieve the current position of a stream. + * + * This method is called in response to {@see fseek()} to determine + * the current position. + * + * @return int should return the current position of the stream + * + * @see https://www.php.net/streamwrapper.stream-tell + */ + public function stream_tell() + { + $pos = ftell($this->fp); + + if ($pos === false) { + throw new \RuntimeException('Cannot get stream position.'); + } + + return $pos; + } + + /** + * Tests for end-of-file on a file pointer. + * + * This method is called in response to {@see feof()}. + * + * @return bool should return TRUE if the read/write position is at + * the end of the stream and if no more data is available + * to be read, or FALSE otherwise + * + * @see https://www.php.net/streamwrapper.stream-eof + */ + public function stream_eof() + { + return feof($this->fp); + } + + /** + * Retrieve information about a file resource. + * + * This method is called in response to {@see fstat()}. + * + * @return array + * + * @see https://www.php.net/streamwrapper.stream-stat + * @see https://www.php.net/stat + * @see https://www.php.net/fstat + */ + public function stream_stat() + { + return fstat($this->fp); + } + + /** + * Flushes the output. + * + * This method is called in response to {@see fflush()} and when the + * stream is being closed while any unflushed data has been written to + * it before. + * If you have cached data in your stream but not yet stored it into + * the underlying storage, you should do so now. + * + * @return bool should return TRUE if the cached data was successfully + * stored (or if there was no data to store), or FALSE + * if the data could not be stored + * + * @see https://www.php.net/streamwrapper.stream-flush + */ + public function stream_flush() + { + return fflush($this->fp); + } + + /** + * Truncate stream. + * + * Will respond to truncation, e.g., through {@see ftruncate()}. + * + * @param int $new_size the new size + * + * @return bool returns TRUE on success or FALSE on failure + * + * @see https://www.php.net/streamwrapper.stream-truncate + */ + public function stream_truncate($new_size) + { + return ftruncate($this->fp, (int) $new_size); + } + + /** + * Write to stream. + * + * This method is called in response to {@see fwrite().} + * + * Note: Remember to update the current position of the stream by + * number of bytes that were successfully written. + * + * @param string $data should be stored into the underlying stream + * + * @return int should return the number of bytes that were successfully stored, or 0 if none could be stored + * + * @see https://www.php.net/streamwrapper.stream-write + */ + public function stream_write($data) + { + $bytes = fwrite($this->fp, $data); + + return $bytes === false ? 0 : $bytes; + } + + /** + * Retrieve the underlaying resource. + * + * This method is called in response to {@see stream_select()}. + * + * @param int $cast_as can be {@see STREAM_CAST_FOR_SELECT} when {@see stream_select()} + * is callingstream_cast() or {@see STREAM_CAST_AS_STREAM} when + * stream_cast() is called for other uses + * + * @return resource + */ + public function stream_cast($cast_as) + { + return $this->fp; + } + + /** + * Close a resource. + * + * This method is called in response to {@see fclose()}. + * All resources that were locked, or allocated, by the wrapper should be released. + * + * @see https://www.php.net/streamwrapper.stream-close + */ + public function stream_close() + { + } +} diff --git a/src/IO/ZipReader.php b/src/IO/ZipReader.php new file mode 100644 index 0000000..764657f --- /dev/null +++ b/src/IO/ZipReader.php @@ -0,0 +1,885 @@ +size = fstat($inStream)['size']; + $this->inStream = $inStream; + + /** @noinspection AdditionOperationOnArraysInspection */ + $options += $this->getDefaultOptions(); + $this->options = $options; + } + + /** + * @return array + */ + protected function getDefaultOptions() + { + return [ + ZipOptions::CHARSET => null, + ]; + } + + /** + * @throws ZipException + * + * @return ImmutableZipContainer + */ + public function read() + { + if ($this->size < ZipConstants::END_CD_MIN_LEN) { + throw new ZipException('Corrupt zip file'); + } + + $endOfCentralDirectory = $this->readEndOfCentralDirectory(); + $entries = $this->readCentralDirectory($endOfCentralDirectory); + + return new ImmutableZipContainer($entries, $endOfCentralDirectory->getComment()); + } + + /** + * @return array + */ + public function getStreamMetaData() + { + return stream_get_meta_data($this->inStream); + } + + /** + * Read End of central directory record. + * + * end of central dir signature 4 bytes (0x06054b50) + * number of this disk 2 bytes + * number of the disk with the + * start of the central directory 2 bytes + * total number of entries in the + * central directory on this disk 2 bytes + * total number of entries in + * the central directory 2 bytes + * size of the central directory 4 bytes + * offset of start of central + * directory with respect to + * the starting disk number 4 bytes + * .ZIP file comment length 2 bytes + * .ZIP file comment (variable size) + * + * @throws ZipException + * + * @return EndOfCentralDirectory + */ + protected function readEndOfCentralDirectory() + { + if (!$this->findEndOfCentralDirectory()) { + throw new ZipException('Invalid zip file. The end of the central directory could not be found.'); + } + + $positionECD = ftell($this->inStream) - 4; + $sizeECD = $this->size - ftell($this->inStream); + $buffer = fread($this->inStream, $sizeECD); + + $unpack = unpack( + 'vdiskNo/vcdDiskNo/vcdEntriesDisk/' . + 'vcdEntries/VcdSize/VcdPos/vcommentLength', + substr($buffer, 0, 18) + ); + + if ( + $unpack['diskNo'] !== 0 || + $unpack['cdDiskNo'] !== 0 || + $unpack['cdEntriesDisk'] !== $unpack['cdEntries'] + ) { + throw new ZipException( + 'ZIP file spanning/splitting is not supported!' + ); + } + // .ZIP file comment (variable sizeECD) + $comment = null; + + if ($unpack['commentLength'] > 0) { + $comment = substr($buffer, 18, $unpack['commentLength']); + } + + // Check for ZIP64 End Of Central Directory Locator exists. + $zip64ECDLocatorPosition = $positionECD - ZipConstants::ZIP64_END_CD_LOC_LEN; + fseek($this->inStream, $zip64ECDLocatorPosition); + // zip64 end of central dir locator + // signature 4 bytes (0x07064b50) + if ($zip64ECDLocatorPosition > 0 && unpack( + 'V', + fread($this->inStream, 4) + )[1] === ZipConstants::ZIP64_END_CD_LOC) { + if (!$this->isZip64Support()) { + throw new ZipException('ZIP64 not supported this archive.'); + } + + $positionECD = $this->findZip64ECDPosition(); + $endCentralDirectory = $this->readZip64EndOfCentralDirectory($positionECD); + $endCentralDirectory->setComment($comment); + } else { + $endCentralDirectory = new EndOfCentralDirectory( + $unpack['cdEntries'], + $unpack['cdPos'], + $unpack['cdSize'], + false, + $comment + ); + } + + return $endCentralDirectory; + } + + /** + * @return bool + */ + protected function findEndOfCentralDirectory() + { + $max = $this->size - ZipConstants::END_CD_MIN_LEN; + $min = $max >= 0xffff ? $max - 0xffff : 0; + // Search for End of central directory record. + for ($position = $max; $position >= $min; $position--) { + fseek($this->inStream, $position); + // end of central dir signature 4 bytes (0x06054b50) + if (unpack('V', fread($this->inStream, 4))[1] !== ZipConstants::END_CD) { + continue; + } + + return true; + } + + return false; + } + + /** + * Read Zip64 end of central directory locator and returns + * Zip64 end of central directory position. + * + * number of the disk with the + * start of the zip64 end of + * central directory 4 bytes + * relative offset of the zip64 + * end of central directory record 8 bytes + * total number of disks 4 bytes + * + * @throws ZipException + * + * @return int Zip64 End Of Central Directory position + */ + protected function findZip64ECDPosition() + { + $data = unpack('VdiskNo/Pzip64ECDPos/VtotalDisks', fread($this->inStream, 16)); + + if ($data['diskNo'] !== 0 || $data['totalDisks'] > 1) { + throw new ZipException('ZIP file spanning/splitting is not supported!'); + } + + return $data['zip64ECDPos']; + } + + /** + * Read zip64 end of central directory locator and zip64 end + * of central directory record. + * + * zip64 end of central dir + * signature 4 bytes (0x06064b50) + * size of zip64 end of central + * directory record 8 bytes + * version made by 2 bytes + * version needed to extract 2 bytes + * number of this disk 4 bytes + * number of the disk with the + * start of the central directory 4 bytes + * total number of entries in the + * central directory on this disk 8 bytes + * total number of entries in the + * central directory 8 bytes + * size of the central directory 8 bytes + * offset of start of central + * directory with respect to + * the starting disk number 8 bytes + * zip64 extensible data sector (variable size) + * + * @param int $zip64ECDPosition + * + * @throws ZipException + * + * @return EndOfCentralDirectory + */ + protected function readZip64EndOfCentralDirectory($zip64ECDPosition) + { + fseek($this->inStream, $zip64ECDPosition); + + $buffer = fread($this->inStream, ZipConstants::ZIP64_END_OF_CD_LEN); + + if (unpack('V', $buffer)[1] !== ZipConstants::ZIP64_END_CD) { + throw new ZipException('Expected ZIP64 End Of Central Directory Record!'); + } + + $data = unpack( +// 'Psize/vversionMadeBy/vextractVersion/' . + 'VdiskNo/VcdDiskNo/PcdEntriesDisk/PentryCount/PcdSize/PcdPos', + substr($buffer, 16) + ); + +// $platform = ZipPlatform::fromValue(($data['versionMadeBy'] & 0xFF00) >> 8); +// $softwareVersion = $data['versionMadeBy'] & 0x00FF; + + if ($data['diskNo'] !== 0 || $data['cdDiskNo'] !== 0 || $data['entryCount'] !== $data['cdEntriesDisk']) { + throw new ZipException('ZIP file spanning/splitting is not supported!'); + } + + if ($data['entryCount'] < 0 || $data['entryCount'] > 0x7fffffff) { + throw new ZipException('Total Number Of Entries In The Central Directory out of range!'); + } + + // skip zip64 extensible data sector (variable sizeEndCD) + + return new EndOfCentralDirectory( + $data['entryCount'], + $data['cdPos'], + $data['cdSize'], + true + ); + } + + /** + * Reads the central directory from the given seekable byte channel + * and populates the internal tables with ZipEntry instances. + * + * The ZipEntry's will know all data that can be obtained from the + * central directory alone, but not the data that requires the local + * file header or additional data to be read. + * + * @param EndOfCentralDirectory $endCD + * + * @throws ZipException + * + * @return ZipEntry[] + */ + protected function readCentralDirectory(EndOfCentralDirectory $endCD) + { + $entries = []; + + $cdOffset = $endCD->getCdOffset(); + fseek($this->inStream, $cdOffset); + + if (!($cdStream = fopen('php://temp', 'w+b'))) { + throw new ZipException('Temp resource can not open from write'); + } + stream_copy_to_stream($this->inStream, $cdStream, $endCD->getCdSize()); + rewind($cdStream); + for ($numEntries = $endCD->getEntryCount(); $numEntries > 0; $numEntries--) { + $zipEntry = $this->readZipEntry($cdStream); + + $entryName = $zipEntry->getName(); + + /** @var UnicodePathExtraField|null $unicodePathExtraField */ + $unicodePathExtraField = $zipEntry->getExtraField(UnicodePathExtraField::HEADER_ID); + + if ($unicodePathExtraField !== null) { + $unicodePath = $unicodePathExtraField->getUnicodeValue(); + + if ($unicodePath !== null) { + $unicodePath = str_replace('\\', '/', $unicodePath); + + if ( + $unicodePath !== '' && + substr_count($entryName, '/') === substr_count($unicodePath, '/') + ) { + $entryName = $unicodePath; + } + } + } + + $entries[$entryName] = $zipEntry; + } + + return $entries; + } + + /** + * Read central directory entry. + * + * central file header signature 4 bytes (0x02014b50) + * version made by 2 bytes + * version needed to extract 2 bytes + * general purpose bit flag 2 bytes + * compression method 2 bytes + * last mod file time 2 bytes + * last mod file date 2 bytes + * crc-32 4 bytes + * compressed size 4 bytes + * uncompressed size 4 bytes + * file name length 2 bytes + * extra field length 2 bytes + * file comment length 2 bytes + * disk number start 2 bytes + * internal file attributes 2 bytes + * external file attributes 4 bytes + * relative offset of local header 4 bytes + * + * file name (variable size) + * extra field (variable size) + * file comment (variable size) + * + * @param resource $stream + * + * @throws ZipException + * + * @return ZipEntry + */ + protected function readZipEntry($stream) + { + if (unpack('V', fread($stream, 4))[1] !== ZipConstants::CENTRAL_FILE_HEADER) { + throw new ZipException('Corrupt zip file. Cannot read zip entry.'); + } + + $unpack = unpack( + 'vversionMadeBy/vversionNeededToExtract/' . + 'vgeneralPurposeBitFlag/vcompressionMethod/' . + 'VlastModFile/Vcrc/VcompressedSize/' . + 'VuncompressedSize/vfileNameLength/vextraFieldLength/' . + 'vfileCommentLength/vdiskNumberStart/vinternalFileAttributes/' . + 'VexternalFileAttributes/VoffsetLocalHeader', + fread($stream, 42) + ); + + if ($unpack['diskNumberStart'] !== 0) { + throw new ZipException('ZIP file spanning/splitting is not supported!'); + } + + $generalPurposeBitFlags = $unpack['generalPurposeBitFlag']; + $isUtf8 = ($generalPurposeBitFlags & GeneralPurposeBitFlag::UTF8) !== 0; + + $name = fread($stream, $unpack['fileNameLength']); + + $createdOS = ($unpack['versionMadeBy'] & 0xFF00) >> 8; + $softwareVersion = $unpack['versionMadeBy'] & 0x00FF; + + $extractedOS = ($unpack['versionNeededToExtract'] & 0xFF00) >> 8; + $extractVersion = $unpack['versionNeededToExtract'] & 0x00FF; + + $dosTime = $unpack['lastModFile']; + + $comment = null; + + if ($unpack['fileCommentLength'] > 0) { + $comment = fread($stream, $unpack['fileCommentLength']); + } + + // decode code page names + $fallbackCharset = null; + + if (!$isUtf8 && isset($this->options[ZipOptions::CHARSET])) { + $charset = $this->options[ZipOptions::CHARSET]; + + $fallbackCharset = $charset; + $name = DosCodePage::toUTF8($name, $charset); + + if ($comment !== null) { + $comment = DosCodePage::toUTF8($comment, $charset); + } + } + + $zipEntry = ZipEntry::create( + $name, + $createdOS, + $extractedOS, + $softwareVersion, + $extractVersion, + $unpack['compressionMethod'], + $generalPurposeBitFlags, + $dosTime, + $unpack['crc'], + $unpack['compressedSize'], + $unpack['uncompressedSize'], + $unpack['internalFileAttributes'], + $unpack['externalFileAttributes'], + $unpack['offsetLocalHeader'], + $comment, + $fallbackCharset + ); + + if ($unpack['extraFieldLength'] > 0) { + $this->parseExtraFields( + fread($stream, $unpack['extraFieldLength']), + $zipEntry, + false + ); + + /** @var Zip64ExtraField|null $extraZip64 */ + $extraZip64 = $zipEntry->getCdExtraField(Zip64ExtraField::HEADER_ID); + + if ($extraZip64 !== null) { + $this->handleZip64Extra($extraZip64, $zipEntry); + } + } + + $this->loadLocalExtraFields($zipEntry); + $this->handleExtraEncryptionFields($zipEntry); + $this->handleExtraFields($zipEntry); + + return $zipEntry; + } + + /** + * @param string $buffer + * @param ZipEntry $zipEntry + * @param bool $local + * + * @return ExtraFieldsCollection + */ + protected function parseExtraFields($buffer, ZipEntry $zipEntry, $local = false) + { + $collection = $local ? + $zipEntry->getLocalExtraFields() : + $zipEntry->getCdExtraFields(); + + if (!empty($buffer)) { + $pos = 0; + $endPos = \strlen($buffer); + + while ($endPos - $pos >= 4) { + /** @var int[] $data */ + $data = unpack('vheaderId/vdataSize', substr($buffer, $pos, 4)); + $pos += 4; + + if ($endPos - $pos - $data['dataSize'] < 0) { + break; + } + $bufferData = substr($buffer, $pos, $data['dataSize']); + $headerId = $data['headerId']; + + /** @var string|ZipExtraField|null $className */ + $className = ZipExtraDriver::getClassNameOrNull($headerId); + + if ($className !== null) { + try { + $extraField = $local ? + \call_user_func([$className, 'unpackLocalFileData'], $bufferData, $zipEntry) : + \call_user_func([$className, 'unpackCentralDirData'], $bufferData, $zipEntry); + } catch (\Throwable $e) { + throw new \RuntimeException( + sprintf( + 'Error parse %s extra field 0x%04X', + $local ? 'local' : 'central directory', + $headerId + ) + ); + } + } else { + $extraField = new UnrecognizedExtraField($headerId, $bufferData); + } + $collection->add($extraField); + $pos += $data['dataSize']; + } + } + + return $collection; + } + + /** + * @param Zip64ExtraField $extraZip64 + * @param ZipEntry $zipEntry + */ + protected function handleZip64Extra(Zip64ExtraField $extraZip64, ZipEntry $zipEntry) + { + $uncompressedSize = $extraZip64->getUncompressedSize(); + $compressedSize = $extraZip64->getCompressedSize(); + $localHeaderOffset = $extraZip64->getLocalHeaderOffset(); + + if ($uncompressedSize !== null) { + $zipEntry->setUncompressedSize($uncompressedSize); + } + + if ($compressedSize !== null) { + $zipEntry->setCompressedSize($compressedSize); + } + + if ($localHeaderOffset !== null) { + $zipEntry->setLocalHeaderOffset($localHeaderOffset); + } + } + + /** + * Read Local File Header. + * + * local file header signature 4 bytes (0x04034b50) + * version needed to extract 2 bytes + * general purpose bit flag 2 bytes + * compression method 2 bytes + * last mod file time 2 bytes + * last mod file date 2 bytes + * crc-32 4 bytes + * compressed size 4 bytes + * uncompressed size 4 bytes + * file name length 2 bytes + * extra field length 2 bytes + * file name (variable size) + * extra field (variable size) + * + * @param ZipEntry $entry + * + * @throws ZipException + */ + protected function loadLocalExtraFields(ZipEntry $entry) + { + $offsetLocalHeader = $entry->getLocalHeaderOffset(); + + fseek($this->inStream, $offsetLocalHeader); + + if (unpack('V', fread($this->inStream, 4))[1] !== ZipConstants::LOCAL_FILE_HEADER) { + throw new ZipException(sprintf('%s (expected Local File Header)', $entry->getName())); + } + + fseek($this->inStream, $offsetLocalHeader + ZipConstants::LFH_FILENAME_LENGTH_POS); + $unpack = unpack('vfileNameLength/vextraFieldLength', fread($this->inStream, 4)); + $offsetData = ftell($this->inStream) + + $unpack['fileNameLength'] + + $unpack['extraFieldLength']; + + fseek($this->inStream, $unpack['fileNameLength'], \SEEK_CUR); + + if ($unpack['extraFieldLength'] > 0) { + $this->parseExtraFields( + fread($this->inStream, $unpack['extraFieldLength']), + $entry, + true + ); + } + + $zipData = new ZipSourceFileData($this, $entry, $offsetData); + $entry->setData($zipData); + } + + /** + * @param ZipEntry $zipEntry + * + * @throws ZipException + */ + private function handleExtraEncryptionFields(ZipEntry $zipEntry) + { + if ($zipEntry->isEncrypted()) { + if ($zipEntry->getCompressionMethod() === ZipCompressionMethod::WINZIP_AES) { + /** @var WinZipAesExtraField|null $extraField */ + $extraField = $zipEntry->getExtraField(WinZipAesExtraField::HEADER_ID); + + if ($extraField === null) { + throw new ZipException( + sprintf( + 'Extra field 0x%04x (WinZip-AES Encryption) expected for compression method %d', + WinZipAesExtraField::HEADER_ID, + $zipEntry->getCompressionMethod() + ) + ); + } + $zipEntry->setCompressionMethod($extraField->getCompressionMethod()); + $zipEntry->setEncryptionMethod($extraField->getEncryptionMethod()); + } else { + $zipEntry->setEncryptionMethod(ZipEncryptionMethod::PKWARE); + } + } + } + + /** + * Handle extra data in zip records. + * + * This is a special method in which you can process ExtraField + * and make changes to ZipEntry. + * + * @param ZipEntry $zipEntry + */ + protected function handleExtraFields(ZipEntry $zipEntry) + { + } + + /** + * @param ZipSourceFileData $zipFileData + * + * @throws ZipException + * @throws Crc32Exception + * + * @return resource + */ + public function getEntryStream(ZipSourceFileData $zipFileData) + { + $outStream = fopen('php://temp', 'w+b'); + $this->copyUncompressedDataToStream($zipFileData, $outStream); + rewind($outStream); + + return $outStream; + } + + /** + * @param ZipSourceFileData $zipFileData + * @param resource $outStream + * + * @throws Crc32Exception + * @throws ZipException + */ + public function copyUncompressedDataToStream(ZipSourceFileData $zipFileData, $outStream) + { + if (!\is_resource($outStream)) { + throw new InvalidArgumentException('outStream is not resource'); + } + + $entry = $zipFileData->getZipEntry(); + +// if ($entry->isDirectory()) { +// throw new InvalidArgumentException('Streams not supported for directories'); +// } + + if ($entry->isStrongEncryption()) { + throw new ZipException('Not support encryption zip.'); + } + + $compressionMethod = $entry->getCompressionMethod(); + + fseek($this->inStream, $zipFileData->getOffset()); + + $filters = []; + + $skipCheckCrc = false; + $isEncrypted = $entry->isEncrypted(); + + if ($isEncrypted) { + if ($entry->getPassword() === null) { + throw new ZipException('Can not password from entry ' . $entry->getName()); + } + + if (ZipEncryptionMethod::isWinZipAesMethod($entry->getEncryptionMethod())) { + /** @var WinZipAesExtraField|null $winZipAesExtra */ + $winZipAesExtra = $entry->getExtraField(WinZipAesExtraField::HEADER_ID); + + if ($winZipAesExtra === null) { + throw new ZipException( + sprintf('WinZip AES must contain the extra field %s', WinZipAesExtraField::HEADER_ID) + ); + } + $compressionMethod = $winZipAesExtra->getCompressionMethod(); + + WinZipAesDecryptionStreamFilter::register(); + $cipherFilterName = WinZipAesDecryptionStreamFilter::FILTER_NAME; + + if ($winZipAesExtra->isV2()) { + $skipCheckCrc = true; + } + } else { + PKDecryptionStreamFilter::register(); + $cipherFilterName = PKDecryptionStreamFilter::FILTER_NAME; + } + $encContextFilter = stream_filter_append( + $this->inStream, + $cipherFilterName, + \STREAM_FILTER_READ, + [ + 'entry' => $entry, + ] + ); + + if (!$encContextFilter) { + throw new \RuntimeException('Not apply filter ' . $cipherFilterName); + } + $filters[] = $encContextFilter; + } + + $contextDecompress = null; + switch ($compressionMethod) { + case ZipCompressionMethod::STORED: + // file without compression, do nothing + break; + + case ZipCompressionMethod::DEFLATED: + if (!($contextDecompress = stream_filter_append( + $this->inStream, + 'zlib.inflate', + \STREAM_FILTER_READ + ))) { + throw new \RuntimeException('Could not append filter "zlib.inflate" to stream'); + } + $filters[] = $contextDecompress; + + break; + + case ZipCompressionMethod::BZIP2: + if (!($contextDecompress = stream_filter_append( + $this->inStream, + 'bzip2.decompress', + \STREAM_FILTER_READ + ))) { + throw new \RuntimeException('Could not append filter "bzip2.decompress" to stream'); + } + $filters[] = $contextDecompress; + + break; + + default: + throw new ZipException( + sprintf( + '%s (compression method %d (%s) is not supported)', + $entry->getName(), + $compressionMethod, + ZipCompressionMethod::getCompressionMethodName($compressionMethod) + ) + ); + } + + $limit = $zipFileData->getUncompressedSize(); + + $offset = 0; + $chunkSize = 8192; + + try { + if ($skipCheckCrc) { + while ($offset < $limit) { + $length = min($chunkSize, $limit - $offset); + $buffer = fread($this->inStream, $length); + + if ($buffer === false) { + throw new ZipException(sprintf('Error reading the contents of entry "%s".', $entry->getName())); + } + fwrite($outStream, $buffer); + $offset += $length; + } + } else { + $contextHash = hash_init('crc32b'); + + while ($offset < $limit) { + $length = min($chunkSize, $limit - $offset); + $buffer = fread($this->inStream, $length); + + if ($buffer === false) { + throw new ZipException(sprintf('Error reading the contents of entry "%s".', $entry->getName())); + } + fwrite($outStream, $buffer); + hash_update($contextHash, $buffer); + $offset += $length; + } + + $expectedCrc = (int) hexdec(hash_final($contextHash)); + + if ($expectedCrc !== $entry->getCrc()) { + throw new Crc32Exception($entry->getName(), $expectedCrc, $entry->getCrc()); + } + } + } finally { + for ($i = \count($filters); $i > 0; $i--) { + stream_filter_remove($filters[$i - 1]); + } + } + } + + /** + * @param ZipSourceFileData $zipData + * @param resource $outStream + */ + public function copyCompressedDataToStream(ZipSourceFileData $zipData, $outStream) + { + if ($zipData->getCompressedSize() > 0) { + fseek($this->inStream, $zipData->getOffset()); + stream_copy_to_stream($this->inStream, $outStream, $zipData->getCompressedSize()); + } + } + + /** + * @return bool + */ + protected function isZip64Support() + { + return \PHP_INT_SIZE === 8; // true for 64bit system + } + + public function close() + { + if (\is_resource($this->inStream)) { + fclose($this->inStream); + } + } + + public function __destruct() + { + $this->close(); + } +} diff --git a/src/IO/ZipWriter.php b/src/IO/ZipWriter.php new file mode 100644 index 0000000..c499f4b --- /dev/null +++ b/src/IO/ZipWriter.php @@ -0,0 +1,886 @@ +zipContainer = $container; + } + + /** + * @param resource $outStream + * + * @throws ZipException + */ + public function write($outStream) + { + if (!\is_resource($outStream)) { + throw new \InvalidArgumentException('$outStream must be resource'); + } + $this->beforeWrite(); + $this->writeLocalBlock($outStream); + $cdOffset = ftell($outStream); + $this->writeCentralDirectoryBlock($outStream); + $cdSize = ftell($outStream) - $cdOffset; + $this->writeEndOfCentralDirectoryBlock($outStream, $cdOffset, $cdSize); + } + + protected function beforeWrite() + { + } + + /** + * @param resource $outStream + * + * @throws ZipException + */ + protected function writeLocalBlock($outStream) + { + $zipEntries = $this->zipContainer->getEntries(); + + foreach ($zipEntries as $zipEntry) { + $this->writeLocalHeader($outStream, $zipEntry); + $this->writeData($outStream, $zipEntry); + + if ($zipEntry->isDataDescriptorEnabled()) { + $this->writeDataDescriptor($outStream, $zipEntry); + } + } + } + + /** + * @param resource $outStream + * @param ZipEntry $entry + * + * @throws ZipException + */ + protected function writeLocalHeader($outStream, ZipEntry $entry) + { + // todo in 4.0 version move zipalign functional to ApkWriter class + if ($this->zipContainer->isZipAlign()) { + $this->zipAlign($outStream, $entry); + } + + $relativeOffset = ftell($outStream); + $entry->setLocalHeaderOffset($relativeOffset); + + if ($entry->isEncrypted() && $entry->getEncryptionMethod() === ZipEncryptionMethod::PKWARE) { + $entry->enableDataDescriptor(true); + } + + $dd = $entry->isDataDescriptorRequired() || + $entry->isDataDescriptorEnabled(); + + $compressedSize = $entry->getCompressedSize(); + $uncompressedSize = $entry->getUncompressedSize(); + + // todo check on 32bit system + $entry->getLocalExtraFields()->remove(Zip64ExtraField::HEADER_ID); + + if ($compressedSize > ZipConstants::ZIP64_MAGIC || $uncompressedSize > ZipConstants::ZIP64_MAGIC) { + $entry->getLocalExtraFields()->add( + new Zip64ExtraField($uncompressedSize, $compressedSize) + ); + + $compressedSize = ZipConstants::ZIP64_MAGIC; + $uncompressedSize = ZipConstants::ZIP64_MAGIC; + } + + $compressionMethod = $entry->getCompressionMethod(); + $crc = $entry->getCrc(); + + if ($entry->isEncrypted() && ZipEncryptionMethod::isWinZipAesMethod($entry->getEncryptionMethod())) { + /** @var WinZipAesExtraField|null $winZipAesExtra */ + $winZipAesExtra = $entry->getLocalExtraField(WinZipAesExtraField::HEADER_ID); + + if ($winZipAesExtra === null) { + $winZipAesExtra = WinZipAesExtraField::create($entry); + } + + if ($winZipAesExtra->isV2()) { + $crc = 0; + } + $compressionMethod = ZipCompressionMethod::WINZIP_AES; + } + + $extra = $this->getExtraFieldsContents($entry, true); + $name = $entry->getName(); + $dosCharset = $entry->getCharset(); + + if ($dosCharset !== null && !$entry->isUtf8Flag()) { + $name = DosCodePage::fromUTF8($name, $dosCharset); + } + + $nameLength = \strlen($name); + $extraLength = \strlen($extra); + + $size = $nameLength + $extraLength; + + if ($size > 0xffff) { + throw new ZipException( + sprintf( + '%s (the total size of %s bytes for the name, extra fields and comment exceeds the maximum size of %d bytes)', + $entry->getName(), + $size, + 0xffff + ) + ); + } + + $extractedBy = ($entry->getExtractedOS() << 8) | $entry->getExtractVersion(); + + fwrite( + $outStream, + pack( + 'VvvvVVVVvv', + // local file header signature 4 bytes (0x04034b50) + ZipConstants::LOCAL_FILE_HEADER, + // version needed to extract 2 bytes + $extractedBy, + // general purpose bit flag 2 bytes + $entry->getGeneralPurposeBitFlags(), + // compression method 2 bytes + $compressionMethod, + // last mod file time 2 bytes + // last mod file date 2 bytes + $entry->getDosTime(), + // crc-32 4 bytes + $dd ? 0 : $crc, + // compressed size 4 bytes + $dd ? 0 : $compressedSize, + // uncompressed size 4 bytes + $dd ? 0 : $uncompressedSize, + // file name length 2 bytes + $nameLength, + // extra field length 2 bytes + $extraLength + ) + ); + + if ($nameLength > 0) { + fwrite($outStream, $name); + } + + if ($extraLength > 0) { + fwrite($outStream, $extra); + } + } + + /** + * @param resource $outStream + * @param ZipEntry $entry + * + * @throws ZipException + */ + private function zipAlign($outStream, ZipEntry $entry) + { + if (!$entry->isDirectory() && $entry->getCompressionMethod() === ZipCompressionMethod::STORED) { + $entry->removeExtraField(ApkAlignmentExtraField::HEADER_ID); + + $extra = $this->getExtraFieldsContents($entry, true); + $extraLength = \strlen($extra); + $name = $entry->getName(); + + $dosCharset = $entry->getCharset(); + + if ($dosCharset !== null && !$entry->isUtf8Flag()) { + $name = DosCodePage::fromUTF8($name, $dosCharset); + } + $nameLength = \strlen($name); + + $multiple = ApkAlignmentExtraField::ALIGNMENT_BYTES; + + if (StringUtil::endsWith($name, '.so')) { + $multiple = ApkAlignmentExtraField::COMMON_PAGE_ALIGNMENT_BYTES; + } + + $offset = ftell($outStream); + + $dataMinStartOffset = + $offset + + ZipConstants::LFH_FILENAME_POS + + $extraLength + + $nameLength; + + $padding = + ($multiple - ($dataMinStartOffset % $multiple)) + % $multiple; + + if ($padding > 0) { + $dataMinStartOffset += ApkAlignmentExtraField::MIN_SIZE; + $padding = + ($multiple - ($dataMinStartOffset % $multiple)) + % $multiple; + + $entry->getLocalExtraFields()->add( + new ApkAlignmentExtraField($multiple, $padding) + ); + } + } + } + + /** + * Merges the local file data fields of the given ZipExtraFields. + * + * @param ZipEntry $entry + * @param bool $local + * + * @throws ZipException + * + * @return string + */ + protected function getExtraFieldsContents(ZipEntry $entry, $local) + { + $local = (bool) $local; + $collection = $local ? + $entry->getLocalExtraFields() : + $entry->getCdExtraFields(); + $extraData = ''; + + foreach ($collection as $extraField) { + if ($local) { + $data = $extraField->packLocalFileData(); + } else { + $data = $extraField->packCentralDirData(); + } + $extraData .= pack( + 'vv', + $extraField->getHeaderId(), + \strlen($data) + ); + $extraData .= $data; + } + + $size = \strlen($extraData); + + if ($size > 0xffff) { + throw new ZipException( + sprintf( + 'Size extra out of range: %d. Extra data: %s', + $size, + $extraData + ) + ); + } + + return $extraData; + } + + /** + * @param resource $outStream + * @param ZipEntry $entry + * + * @throws ZipException + */ + protected function writeData($outStream, ZipEntry $entry) + { + $zipData = $entry->getData(); + + if ($zipData === null) { + if ($entry->isDirectory()) { + return; + } + + throw new ZipException(sprintf('No zip data for entry "%s"', $entry->getName())); + } + + // data write variants: + // -------------------- + // * data of source zip file -> copy compressed data + // * store - simple write + // * store and encryption - apply encryption filter and simple write + // * deflate or bzip2 - apply compression filter and simple write + // * (deflate or bzip2) and encryption - create temp stream and apply + // compression filter to it, then apply encryption filter to root + // stream and write temp stream data. + // (PHP cannot apply the filter for encryption after the compression + // filter, so a temporary stream is created for the compressed data) + + if ($zipData instanceof ZipSourceFileData && !$this->zipContainer->hasRecompressData($entry)) { + // data of source zip file -> copy compressed data + $zipData->copyCompressedDataToStream($outStream); + + return; + } + + $entryStream = $zipData->getDataAsStream(); + + if (stream_get_meta_data($entryStream)['seekable']) { + rewind($entryStream); + } + + $uncompressedSize = $entry->getUncompressedSize(); + + $posBeforeWrite = ftell($outStream); + $compressionMethod = $entry->getCompressionMethod(); + + if ($entry->isEncrypted()) { + if ($compressionMethod === ZipCompressionMethod::STORED) { + $contextFilter = $this->appendEncryptionFilter($outStream, $entry, $uncompressedSize); + $checksum = $this->writeAndCountChecksum($entryStream, $outStream, $uncompressedSize); + } else { + $compressStream = fopen('php://temp', 'w+b'); + $contextFilter = $this->appendCompressionFilter($compressStream, $entry); + $checksum = $this->writeAndCountChecksum($entryStream, $compressStream, $uncompressedSize); + + if ($contextFilter !== null) { + stream_filter_remove($contextFilter); + $contextFilter = null; + } + + rewind($compressStream); + + $compressedSize = fstat($compressStream)['size']; + $contextFilter = $this->appendEncryptionFilter($outStream, $entry, $compressedSize); + + stream_copy_to_stream($compressStream, $outStream); + } + } else { + $contextFilter = $this->appendCompressionFilter($outStream, $entry); + $checksum = $this->writeAndCountChecksum($entryStream, $outStream, $uncompressedSize); + } + + if ($contextFilter !== null) { + stream_filter_remove($contextFilter); + $contextFilter = null; + } + + // my hack {@see https://bugs.php.net/bug.php?id=49874} + fseek($outStream, 0, \SEEK_END); + $compressedSize = ftell($outStream) - $posBeforeWrite; + + $entry->setCompressedSize($compressedSize); + $entry->setCrc($checksum); + + if (!$entry->isDataDescriptorEnabled()) { + if ($uncompressedSize > ZipConstants::ZIP64_MAGIC || $compressedSize > ZipConstants::ZIP64_MAGIC) { + /** @var Zip64ExtraField|null $zip64ExtraLocal */ + $zip64ExtraLocal = $entry->getLocalExtraField(Zip64ExtraField::HEADER_ID); + + // if there is a zip64 extra record, then update it; + // if not, write data to data descriptor + if ($zip64ExtraLocal !== null) { + $zip64ExtraLocal->setCompressedSize($compressedSize); + $zip64ExtraLocal->setUncompressedSize($uncompressedSize); + + $posExtra = $entry->getLocalHeaderOffset() + ZipConstants::LFH_FILENAME_POS + \strlen($entry->getName()); + fseek($outStream, $posExtra); + fwrite($outStream, $this->getExtraFieldsContents($entry, true)); + } else { + $posGPBF = $entry->getLocalHeaderOffset() + 6; + $entry->enableDataDescriptor(true); + fseek($outStream, $posGPBF); + fwrite( + $outStream, + pack( + 'v', + // general purpose bit flag 2 bytes + $entry->getGeneralPurposeBitFlags() + ) + ); + } + + $compressedSize = ZipConstants::ZIP64_MAGIC; + $uncompressedSize = ZipConstants::ZIP64_MAGIC; + } + + $posChecksum = $entry->getLocalHeaderOffset() + 14; + + /** @var WinZipAesExtraField|null $winZipAesExtra */ + $winZipAesExtra = $entry->getLocalExtraField(WinZipAesExtraField::HEADER_ID); + + if ($winZipAesExtra !== null && $winZipAesExtra->isV2()) { + $checksum = 0; + } + + fseek($outStream, $posChecksum); + fwrite( + $outStream, + pack( + 'VVV', + // crc-32 4 bytes + $checksum, + // compressed size 4 bytes + $compressedSize, + // uncompressed size 4 bytes + $uncompressedSize + ) + ); + fseek($outStream, 0, \SEEK_END); + } + } + + /** + * @param resource $inStream + * @param resource $outStream + * @param int $size + * + * @return int + */ + private function writeAndCountChecksum($inStream, $outStream, $size) + { + $contextHash = hash_init('crc32b'); + $offset = 0; + + while ($offset < $size) { + $read = min(self::CHUNK_SIZE, $size - $offset); + $buffer = fread($inStream, $read); + fwrite($outStream, $buffer); + hash_update($contextHash, $buffer); + $offset += $read; + } + + return (int) hexdec(hash_final($contextHash)); + } + + /** + * @param resource $outStream + * @param ZipEntry $entry + * + * @throws ZipUnsupportMethodException + * + * @return resource|null + */ + protected function appendCompressionFilter($outStream, ZipEntry $entry) + { + $contextCompress = null; + switch ($entry->getCompressionMethod()) { + case ZipCompressionMethod::DEFLATED: + if (!($contextCompress = stream_filter_append( + $outStream, + 'zlib.deflate', + \STREAM_FILTER_WRITE, + ['level' => $entry->getCompressionLevel()] + ))) { + throw new \RuntimeException('Could not append filter "zlib.deflate" to out stream'); + } + break; + + case ZipCompressionMethod::BZIP2: + if (!($contextCompress = stream_filter_append( + $outStream, + 'bzip2.compress', + \STREAM_FILTER_WRITE, + ['blocks' => $entry->getCompressionLevel(), 'work' => 0] + ))) { + throw new \RuntimeException('Could not append filter "bzip2.compress" to out stream'); + } + break; + + case ZipCompressionMethod::STORED: + // file without compression, do nothing + break; + + default: + throw new ZipUnsupportMethodException( + sprintf( + '%s (compression method %d (%s) is not supported)', + $entry->getName(), + $entry->getCompressionMethod(), + ZipCompressionMethod::getCompressionMethodName($entry->getCompressionMethod()) + ) + ); + } + + return $contextCompress; + } + + /** + * @param resource $outStream + * @param ZipEntry $entry + * @param int $size + * + * @return resource|null + */ + protected function appendEncryptionFilter($outStream, ZipEntry $entry, $size) + { + $encContextFilter = null; + + if ($entry->isEncrypted()) { + if ($entry->getEncryptionMethod() === ZipEncryptionMethod::PKWARE) { + PKEncryptionStreamFilter::register(); + $cipherFilterName = PKEncryptionStreamFilter::FILTER_NAME; + } else { + WinZipAesEncryptionStreamFilter::register(); + $cipherFilterName = WinZipAesEncryptionStreamFilter::FILTER_NAME; + } + $encContextFilter = stream_filter_append( + $outStream, + $cipherFilterName, + \STREAM_FILTER_WRITE, + [ + 'entry' => $entry, + 'size' => $size, + ] + ); + + if (!$encContextFilter) { + throw new \RuntimeException('Not apply filter ' . $cipherFilterName); + } + } + + return $encContextFilter; + } + + /** + * @param resource $outStream + * @param ZipEntry $entry + */ + protected function writeDataDescriptor($outStream, ZipEntry $entry) + { + $crc = $entry->getCrc(); + + /** @var WinZipAesExtraField|null $winZipAesExtra */ + $winZipAesExtra = $entry->getLocalExtraField(WinZipAesExtraField::HEADER_ID); + + if ($winZipAesExtra !== null && $winZipAesExtra->isV2()) { + $crc = 0; + } + + fwrite( + $outStream, + pack( + 'VV', + // data descriptor signature 4 bytes (0x08074b50) + ZipConstants::DATA_DESCRIPTOR, + // crc-32 4 bytes + $crc + ) + ); + + if ( + $entry->isZip64ExtensionsRequired() || + $entry->getLocalExtraFields()->has(Zip64ExtraField::HEADER_ID) + ) { + $dd = + // compressed size 8 bytes + PackUtil::packLongLE($entry->getCompressedSize()) . + // uncompressed size 8 bytes + PackUtil::packLongLE($entry->getUncompressedSize()); + } else { + $dd = pack( + 'VV', + // compressed size 4 bytes + $entry->getCompressedSize(), + // uncompressed size 4 bytes + $entry->getUncompressedSize() + ); + } + + fwrite($outStream, $dd); + } + + /** + * @param resource $outStream + * + * @throws ZipException + */ + protected function writeCentralDirectoryBlock($outStream) + { + foreach ($this->zipContainer->getEntries() as $outputEntry) { + $this->writeCentralDirectoryHeader($outStream, $outputEntry); + } + } + + /** + * Writes a Central File Header record. + * + * @param resource $outStream + * @param ZipEntry $entry + * + * @throws ZipException + */ + protected function writeCentralDirectoryHeader($outStream, ZipEntry $entry) + { + $compressedSize = $entry->getCompressedSize(); + $uncompressedSize = $entry->getUncompressedSize(); + $localHeaderOffset = $entry->getLocalHeaderOffset(); + + // todo check on 32bit system + $entry->getCdExtraFields()->remove(Zip64ExtraField::HEADER_ID); + + if ( + $localHeaderOffset > ZipConstants::ZIP64_MAGIC || + $compressedSize > ZipConstants::ZIP64_MAGIC || + $uncompressedSize > ZipConstants::ZIP64_MAGIC + ) { + $zip64ExtraField = new Zip64ExtraField(); + + if ($uncompressedSize >= ZipConstants::ZIP64_MAGIC) { + $zip64ExtraField->setUncompressedSize($uncompressedSize); + $uncompressedSize = ZipConstants::ZIP64_MAGIC; + } + + if ($compressedSize >= ZipConstants::ZIP64_MAGIC) { + $zip64ExtraField->setCompressedSize($compressedSize); + $compressedSize = ZipConstants::ZIP64_MAGIC; + } + + if ($localHeaderOffset >= ZipConstants::ZIP64_MAGIC) { + $zip64ExtraField->setLocalHeaderOffset($localHeaderOffset); + $localHeaderOffset = ZipConstants::ZIP64_MAGIC; + } + + $entry->getCdExtraFields()->add($zip64ExtraField); + } + + $extra = $this->getExtraFieldsContents($entry, false); + $extraLength = \strlen($extra); + + $name = $entry->getName(); + $comment = $entry->getComment(); + + $dosCharset = $entry->getCharset(); + + if ($dosCharset !== null && !$entry->isUtf8Flag()) { + $name = DosCodePage::fromUTF8($name, $dosCharset); + + if ($comment) { + $comment = DosCodePage::fromUTF8($comment, $dosCharset); + } + } + + $commentLength = \strlen($comment); + + $compressionMethod = $entry->getCompressionMethod(); + $crc = $entry->getCrc(); + + /** @var WinZipAesExtraField|null $winZipAesExtra */ + $winZipAesExtra = $entry->getLocalExtraField(WinZipAesExtraField::HEADER_ID); + + if ($winZipAesExtra !== null) { + if ($winZipAesExtra->isV2()) { + $crc = 0; + } + $compressionMethod = ZipCompressionMethod::WINZIP_AES; + } + + fwrite( + $outStream, + pack( + 'VvvvvVVVVvvvvvVV', + // central file header signature 4 bytes (0x02014b50) + ZipConstants::CENTRAL_FILE_HEADER, + // version made by 2 bytes + ($entry->getCreatedOS() << 8) | $entry->getSoftwareVersion(), + // version needed to extract 2 bytes + ($entry->getExtractedOS() << 8) | $entry->getExtractVersion(), + // general purpose bit flag 2 bytes + $entry->getGeneralPurposeBitFlags(), + // compression method 2 bytes + $compressionMethod, + // last mod file datetime 4 bytes + $entry->getDosTime(), + // crc-32 4 bytes + $crc, + // compressed size 4 bytes + $compressedSize, + // uncompressed size 4 bytes + $uncompressedSize, + // file name length 2 bytes + \strlen($name), + // extra field length 2 bytes + $extraLength, + // file comment length 2 bytes + $commentLength, + // disk number start 2 bytes + 0, + // internal file attributes 2 bytes + $entry->getInternalAttributes(), + // external file attributes 4 bytes + $entry->getExternalAttributes(), + // relative offset of local header 4 bytes + $localHeaderOffset + ) + ); + + // file name (variable size) + fwrite($outStream, $name); + + if ($extraLength > 0) { + // extra field (variable size) + fwrite($outStream, $extra); + } + + if ($commentLength > 0) { + // file comment (variable size) + fwrite($outStream, $comment); + } + } + + /** + * @param resource $outStream + * @param int $centralDirectoryOffset + * @param int $centralDirectorySize + */ + protected function writeEndOfCentralDirectoryBlock( + $outStream, + $centralDirectoryOffset, + $centralDirectorySize + ) { + $cdEntriesCount = \count($this->zipContainer); + + $cdEntriesZip64 = $cdEntriesCount > 0xffff; + $cdSizeZip64 = $centralDirectorySize > ZipConstants::ZIP64_MAGIC; + $cdOffsetZip64 = $centralDirectoryOffset > ZipConstants::ZIP64_MAGIC; + + $zip64Required = $cdEntriesZip64 + || $cdSizeZip64 + || $cdOffsetZip64; + + if ($zip64Required) { + $zip64EndOfCentralDirectoryOffset = ftell($outStream); + + // find max software version, version needed to extract and most common platform + list($softwareVersion, $versionNeededToExtract) = array_reduce( + $this->zipContainer->getEntries(), + static function (array $carry, ZipEntry $entry) { + $carry[0] = max($carry[0], $entry->getSoftwareVersion() & 0xFF); + $carry[1] = max($carry[1], $entry->getExtractVersion() & 0xFF); + + return $carry; + }, + [ZipVersion::v10_DEFAULT_MIN, ZipVersion::v45_ZIP64_EXT] + ); + + $createdOS = $extractedOS = ZipPlatform::OS_DOS; + $versionMadeBy = ($createdOS << 8) | max($softwareVersion, ZipVersion::v45_ZIP64_EXT); + $versionExtractedBy = ($extractedOS << 8) | max($versionNeededToExtract, ZipVersion::v45_ZIP64_EXT); + + // write zip64 end of central directory signature + fwrite( + $outStream, + pack( + 'V', + // signature 4 bytes (0x06064b50) + ZipConstants::ZIP64_END_CD + ) + ); + // size of zip64 end of central + // directory record 8 bytes + fwrite($outStream, PackUtil::packLongLE(ZipConstants::ZIP64_END_OF_CD_LEN - 12)); + fwrite( + $outStream, + pack( + 'vvVV', + // version made by 2 bytes + $versionMadeBy & 0xFFFF, + // version needed to extract 2 bytes + $versionExtractedBy & 0xFFFF, + // number of this disk 4 bytes + 0, + // number of the disk with the + // start of the central directory 4 bytes + 0 + ) + ); + + fwrite( + $outStream, + // total number of entries in the + // central directory on this disk 8 bytes + PackUtil::packLongLE($cdEntriesCount) . + // total number of entries in the + // central directory 8 bytes + PackUtil::packLongLE($cdEntriesCount) . + // size of the central directory 8 bytes + PackUtil::packLongLE($centralDirectorySize) . + // offset of start of central + // directory with respect to + // the starting disk number 8 bytes + PackUtil::packLongLE($centralDirectoryOffset) + ); + + // write zip64 end of central directory locator + fwrite( + $outStream, + pack( + 'VV', + // zip64 end of central dir locator + // signature 4 bytes (0x07064b50) + ZipConstants::ZIP64_END_CD_LOC, + // number of the disk with the + // start of the zip64 end of + // central directory 4 bytes + 0 + ) . + // relative offset of the zip64 + // end of central directory record 8 bytes + PackUtil::packLongLE($zip64EndOfCentralDirectoryOffset) . + // total number of disks 4 bytes + pack('V', 1) + ); + } + + $comment = $this->zipContainer->getArchiveComment(); + $commentLength = $comment !== null ? \strlen($comment) : 0; + + fwrite( + $outStream, + pack( + 'VvvvvVVv', + // end of central dir signature 4 bytes (0x06054b50) + ZipConstants::END_CD, + // number of this disk 2 bytes + 0, + // number of the disk with the + // start of the central directory 2 bytes + 0, + // total number of entries in the + // central directory on this disk 2 bytes + $cdEntriesZip64 ? 0xffff : $cdEntriesCount, + // total number of entries in + // the central directory 2 bytes + $cdEntriesZip64 ? 0xffff : $cdEntriesCount, + // size of the central directory 4 bytes + $cdSizeZip64 ? ZipConstants::ZIP64_MAGIC : $centralDirectorySize, + // offset of start of central + // directory with respect to + // the starting disk number 4 bytes + $cdOffsetZip64 ? ZipConstants::ZIP64_MAGIC : $centralDirectoryOffset, + // .ZIP file comment length 2 bytes + $commentLength + ) + ); + + if ($comment !== null && $commentLength > 0) { + // .ZIP file comment (variable size) + fwrite($outStream, $comment); + } + } +} diff --git a/src/Model/Data/ZipFileData.php b/src/Model/Data/ZipFileData.php new file mode 100644 index 0000000..9a80890 --- /dev/null +++ b/src/Model/Data/ZipFileData.php @@ -0,0 +1,78 @@ +isFile()) { + throw new ZipException('$fileInfo is not a file.'); + } + + if (!$fileInfo->isReadable()) { + throw new ZipException('$fileInfo is not readable.'); + } + + $this->file = $fileInfo; + } + + /** + * @throws ZipException + * + * @return resource returns stream data + */ + public function getDataAsStream() + { + if (!$this->file->isReadable()) { + throw new ZipException(sprintf('The %s file is no longer readable.', $this->file->getPathname())); + } + + return fopen($this->file->getPathname(), 'rb'); + } + + /** + * @throws ZipException + * + * @return string returns data as string + */ + public function getDataAsString() + { + if (!$this->file->isReadable()) { + throw new ZipException(sprintf('The %s file is no longer readable.', $this->file->getPathname())); + } + + return file_get_contents($this->file->getPathname()); + } + + /** + * @param resource $outStream + * + * @throws ZipException + */ + public function copyDataToStream($outStream) + { + try { + $stream = $this->getDataAsStream(); + stream_copy_to_stream($stream, $outStream); + } finally { + fclose($stream); + } + } +} diff --git a/src/Model/Data/ZipNewData.php b/src/Model/Data/ZipNewData.php new file mode 100644 index 0000000..4b1dd0e --- /dev/null +++ b/src/Model/Data/ZipNewData.php @@ -0,0 +1,88 @@ +zipEntry = $zipEntry; + + if (\is_string($data)) { + $zipEntry->setUncompressedSize(\strlen($data)); + + if (!($handle = fopen('php://temp', 'w+b'))) { + throw new \RuntimeException('Temp resource can not open from write.'); + } + fwrite($handle, $data); + rewind($handle); + $this->stream = $handle; + } elseif (\is_resource($data)) { + $this->stream = $data; + } + } + + /** + * @return resource returns stream data + */ + public function getDataAsStream() + { + if (!\is_resource($this->stream)) { + throw new \LogicException(sprintf('Resource was closed (entry=%s).', $this->zipEntry->getName())); + } + + return $this->stream; + } + + /** + * @return string returns data as string + */ + public function getDataAsString() + { + $stream = $this->getDataAsStream(); + $pos = ftell($stream); + + try { + rewind($stream); + + return stream_get_contents($stream); + } finally { + fseek($stream, $pos); + } + } + + /** + * @param resource $outStream + */ + public function copyDataToStream($outStream) + { + $stream = $this->getDataAsStream(); + rewind($stream); + stream_copy_to_stream($stream, $outStream); + } + + public function __destruct() + { + if (\is_resource($this->stream)) { + fclose($this->stream); + } + } +} diff --git a/src/Model/Data/ZipSourceFileData.php b/src/Model/Data/ZipSourceFileData.php new file mode 100644 index 0000000..fc2328f --- /dev/null +++ b/src/Model/Data/ZipSourceFileData.php @@ -0,0 +1,155 @@ +zipReader = $zipReader; + $this->offset = $offsetData; + $this->zipEntry = $zipEntry; + $this->compressedSize = $zipEntry->getCompressedSize(); + $this->uncompressedSize = $zipEntry->getUncompressedSize(); + } + + /** + * @throws ZipException + * + * @return resource returns stream data + */ + public function getDataAsStream() + { + if (!\is_resource($this->stream)) { + $this->stream = $this->zipReader->getEntryStream($this); + } + + return $this->stream; + } + + /** + * @throws ZipException + * + * @return string returns data as string + */ + public function getDataAsString() + { + $autoClosable = $this->stream === null; + + $stream = $this->getDataAsStream(); + $pos = ftell($stream); + + try { + rewind($stream); + + return stream_get_contents($stream); + } finally { + if ($autoClosable) { + fclose($stream); + $this->stream = null; + } else { + fseek($stream, $pos); + } + } + } + + /** + * @param resource $outputStream Output stream + * + * @throws ZipException + * @throws Crc32Exception + */ + public function copyDataToStream($outputStream) + { + if (\is_resource($this->stream)) { + rewind($this->stream); + stream_copy_to_stream($this->stream, $outputStream); + } else { + $this->zipReader->copyUncompressedDataToStream($this, $outputStream); + } + } + + /** + * @param resource $outputStream Output stream + */ + public function copyCompressedDataToStream($outputStream) + { + $this->zipReader->copyCompressedDataToStream($this, $outputStream); + } + + /** + * @return ZipEntry + */ + public function getZipEntry() + { + return $this->zipEntry; + } + + /** + * @return int + */ + public function getCompressedSize() + { + return $this->compressedSize; + } + + /** + * @return int + */ + public function getUncompressedSize() + { + return $this->uncompressedSize; + } + + /** + * @return int + */ + public function getOffset() + { + return $this->offset; + } + + /** + * {@inheritdoc} + */ + public function __destruct() + { + if (\is_resource($this->stream)) { + fclose($this->stream); + } + } +} diff --git a/src/Model/EndOfCentralDirectory.php b/src/Model/EndOfCentralDirectory.php new file mode 100644 index 0000000..d312cfe --- /dev/null +++ b/src/Model/EndOfCentralDirectory.php @@ -0,0 +1,93 @@ +entryCount = $entryCount; + $this->cdOffset = $cdOffset; + $this->cdSize = $cdSize; + $this->zip64 = $zip64; + $this->comment = $comment; + } + + /** + * @param string|null $comment + */ + public function setComment($comment) + { + $this->comment = $comment; + } + + /** + * @return int + */ + public function getEntryCount() + { + return $this->entryCount; + } + + /** + * @return int + */ + public function getCdOffset() + { + return $this->cdOffset; + } + + /** + * @return int + */ + public function getCdSize() + { + return $this->cdSize; + } + + /** + * @return string|null + */ + public function getComment() + { + return $this->comment; + } + + /** + * @return bool + */ + public function isZip64() + { + return $this->zip64; + } +} diff --git a/src/PhpZip/Extra/ExtraFieldsCollection.php b/src/Model/Extra/ExtraFieldsCollection.php similarity index 52% rename from src/PhpZip/Extra/ExtraFieldsCollection.php rename to src/Model/Extra/ExtraFieldsCollection.php index 24345ea..9cc2020 100644 --- a/src/PhpZip/Extra/ExtraFieldsCollection.php +++ b/src/Model/Extra/ExtraFieldsCollection.php @@ -1,18 +1,12 @@ 0xffff) { - throw new ZipException('headerId out of range'); - } + $this->validateHeaderId($headerId); - if (isset($this->collection[$headerId])) { - return $this->collection[$headerId]; - } + return isset($this->collection[$headerId]) ? $this->collection[$headerId] : null; + } - return null; + /** + * @param int $headerId + */ + private function validateHeaderId($headerId) + { + if ($headerId < 0 || $headerId > 0xffff) { + throw new \InvalidArgumentException('$headerId out of range'); + } } /** * Stores the given Extra Field in this collection. * - * @param ExtraField $extraField the Extra Field to store in this collection + * @param ZipExtraField $extraField the Extra Field to store in this collection * - * @throws ZipException if headerId is out of range - * - * @return ExtraField the Extra Field previously associated with the Header ID of - * of the given Extra Field or null if no such Extra Field existed + * @return ZipExtraField the Extra Field previously associated with the Header ID of + * of the given Extra Field or null if no such Extra Field existed */ - public function add(ExtraField $extraField) + public function add(ZipExtraField $extraField) { - $headerId = $extraField::getHeaderId(); + $headerId = $extraField->getHeaderId(); - if ($headerId < 0x0000 || $headerId > 0xffff) { - throw new ZipException('headerId out of range'); - } + $this->validateHeaderId($headerId); $this->collection[$headerId] = $extraField; return $extraField; } + /** + * @param ZipExtraField[] $extraFields + */ + public function addAll(array $extraFields) + { + foreach ($extraFields as $extraField) { + $this->add($extraField); + } + } + + /** + * @param ExtraFieldsCollection $collection + */ + public function addCollection(self $collection) + { + $this->addAll($collection->collection); + } + + /** + * @return ZipExtraField[] + */ + public function getAll() + { + return $this->collection; + } + /** * Returns Extra Field exists. * @@ -97,16 +115,12 @@ class ExtraFieldsCollection implements \Countable, \ArrayAccess, \Iterator * * @param int $headerId the requested Header ID * - * @throws ZipException if headerId is out of range or extra field not found - * - * @return ExtraField the Extra Field with the given Header ID or null - * if no such Extra Field exists + * @return ZipExtraField|null the Extra Field with the given Header ID or null + * if no such Extra Field exists */ public function remove($headerId) { - if ($headerId < 0x0000 || $headerId > 0xffff) { - throw new ZipException('headerId out of range'); - } + $this->validateHeaderId($headerId); if (isset($this->collection[$headerId])) { $ef = $this->collection[$headerId]; @@ -115,7 +129,7 @@ class ExtraFieldsCollection implements \Countable, \ArrayAccess, \Iterator return $ef; } - throw new ZipException('ExtraField not found'); + return null; } /** @@ -123,20 +137,13 @@ class ExtraFieldsCollection implements \Countable, \ArrayAccess, \Iterator * * @see http://php.net/manual/en/arrayaccess.offsetexists.php * - * @param mixed $offset

- * An offset to check for. - *

+ * @param int $offset an offset to check for * - * @return bool true on success or false on failure. - *

- *

- * The return value will be casted to boolean if non-boolean was returned. - * - * @since 5.0.0 + * @return bool true on success or false on failure */ public function offsetExists($offset) { - return isset($this->collection[$offset]); + return isset($this->collection[(int) $offset]); } /** @@ -144,19 +151,13 @@ class ExtraFieldsCollection implements \Countable, \ArrayAccess, \Iterator * * @see http://php.net/manual/en/arrayaccess.offsetget.php * - * @param mixed $offset

- * The offset to retrieve. - *

+ * @param int $offset the offset to retrieve * - * @throws ZipException - * - * @return mixed can return all value types - * - * @since 5.0.0 + * @return ZipExtraField|null */ public function offsetGet($offset) { - return $this->get($offset); + return isset($this->collection[$offset]) ? $this->collection[$offset] : null; } /** @@ -164,27 +165,15 @@ class ExtraFieldsCollection implements \Countable, \ArrayAccess, \Iterator * * @see 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. - *

- * - * @throws ZipException - * - * @since 5.0.0 + * @param mixed $offset the offset to assign the value to + * @param ZipExtraField $value the value to set */ public function offsetSet($offset, $value) { - if ($value instanceof ExtraField) { - if ($offset !== $value::getHeaderId()) { - throw new InvalidArgumentException('Value header id !== array access key'); - } - $this->add($value); - } else { - throw new InvalidArgumentException('value is not instanceof ' . ExtraField::class); + if (!$value instanceof ZipExtraField) { + throw new \InvalidArgumentException('value is not instanceof ' . ZipExtraField::class); } + $this->add($value); } /** @@ -192,13 +181,7 @@ class ExtraFieldsCollection implements \Countable, \ArrayAccess, \Iterator * * @see http://php.net/manual/en/arrayaccess.offsetunset.php * - * @param mixed $offset

- * The offset to unset. - *

- * - * @since 5.0.0 - * - * @throws ZipException + * @param mixed $offset the offset to unset */ public function offsetUnset($offset) { @@ -210,9 +193,7 @@ class ExtraFieldsCollection implements \Countable, \ArrayAccess, \Iterator * * @see http://php.net/manual/en/iterator.current.php * - * @return mixed can return any type - * - * @since 5.0.0 + * @return ZipExtraField */ public function current() { @@ -223,7 +204,6 @@ class ExtraFieldsCollection implements \Countable, \ArrayAccess, \Iterator * Move forward to next element. * * @see http://php.net/manual/en/iterator.next.php - * @since 5.0.0 */ public function next() { @@ -235,9 +215,7 @@ class ExtraFieldsCollection implements \Countable, \ArrayAccess, \Iterator * * @see http://php.net/manual/en/iterator.key.php * - * @return mixed scalar on success, or null on failure - * - * @since 5.0.0 + * @return int scalar on success, or null on failure */ public function key() { @@ -251,25 +229,41 @@ class ExtraFieldsCollection implements \Countable, \ArrayAccess, \Iterator * * @return bool 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()); + return key($this->collection) !== null; } /** * Rewind the Iterator to the first element. * * @see http://php.net/manual/en/iterator.rewind.php - * @since 5.0.0 */ public function rewind() { reset($this->collection); } + public function clear() + { + $this->collection = []; + } + + /** + * @return string + */ + public function __toString() + { + $formats = []; + + foreach ($this->collection as $key => $value) { + $formats[] = (string) $value; + } + + return implode("\n", $formats); + } + /** * If clone extra fields. */ diff --git a/src/Model/Extra/Fields/AbstractUnicodeExtraField.php b/src/Model/Extra/Fields/AbstractUnicodeExtraField.php new file mode 100644 index 0000000..6587a82 --- /dev/null +++ b/src/Model/Extra/Fields/AbstractUnicodeExtraField.php @@ -0,0 +1,137 @@ +crc32 = (int) $crc32; + $this->unicodeValue = (string) $unicodeValue; + } + + /** + * @param string $unicodeValue + * + * @return static + */ + public static function create($unicodeValue) + { + return new static(crc32($unicodeValue), $unicodeValue); + } + + /** + * @return int the CRC32 checksum of the filename or comment as + * encoded in the central directory of the zip file + */ + public function getCrc32() + { + return $this->crc32; + } + + /** + * @return string + */ + public function getUnicodeValue() + { + return $this->unicodeValue; + } + + /** + * @param string $unicodeValue the UTF-8 encoded name to set + */ + public function setUnicodeValue($unicodeValue) + { + $this->unicodeValue = $unicodeValue; + $this->crc32 = crc32($unicodeValue); + } + + /** + * Populate data from this array as if it was in local file data. + * + * @param string $buffer the buffer to read data from + * @param ZipEntry|null $entry + * + * @throws ZipException on error + * + * @return static + */ + public static function unpackLocalFileData($buffer, ZipEntry $entry = null) + { + if (\strlen($buffer) < 5) { + throw new ZipException('UniCode path extra data must have at least 5 bytes.'); + } + + $version = unpack('C', $buffer)[1]; + + if ($version !== self::DEFAULT_VERSION) { + throw new ZipException(sprintf('Unsupported version [%d] for UniCode path extra data.', $version)); + } + + $crc32 = unpack('V', substr($buffer, 1))[1]; + $unicodeValue = substr($buffer, 5); + + return new static($crc32, $unicodeValue); + } + + /** + * Populate data from this array as if it was in central directory data. + * + * @param string $buffer the buffer to read data from + * @param ZipEntry|null $entry + * + * @throws ZipException on error + * + * @return static + */ + public static function unpackCentralDirData($buffer, ZipEntry $entry = null) + { + return self::unpackLocalFileData($buffer, $entry); + } + + /** + * The actual data to put into local file data - without Header-ID + * or length specifier. + * + * @return string the data + */ + public function packLocalFileData() + { + return pack( + 'CV', + self::DEFAULT_VERSION, + $this->crc32 + ) . + $this->unicodeValue; + } + + /** + * The actual data to put into central directory - without Header-ID or + * length specifier. + * + * @return string the data + */ + public function packCentralDirData() + { + return $this->packLocalFileData(); + } +} diff --git a/src/Model/Extra/Fields/ApkAlignmentExtraField.php b/src/Model/Extra/Fields/ApkAlignmentExtraField.php new file mode 100644 index 0000000..69c26b8 --- /dev/null +++ b/src/Model/Extra/Fields/ApkAlignmentExtraField.php @@ -0,0 +1,176 @@ +multiple = $multiple; + $this->padding = $padding; + } + + /** + * Returns the Header ID (type) of this Extra Field. + * The Header ID is an unsigned short integer (two bytes) + * which must be constant during the life cycle of this object. + * + * @return int + */ + public function getHeaderId() + { + return self::HEADER_ID; + } + + /** + * @return int + */ + public function getMultiple() + { + return $this->multiple; + } + + /** + * @return int + */ + public function getPadding() + { + return $this->padding; + } + + /** + * @param int $multiple + */ + public function setMultiple($multiple) + { + $this->multiple = (int) $multiple; + } + + /** + * @param int $padding + */ + public function setPadding($padding) + { + $this->padding = (int) $padding; + } + + /** + * Populate data from this array as if it was in local file data. + * + * @param string $buffer the buffer to read data from + * @param ZipEntry|null $entry + * + * @throws ZipException + * + * @return ApkAlignmentExtraField + */ + public static function unpackLocalFileData($buffer, ZipEntry $entry = null) + { + $length = \strlen($buffer); + + if ($length < 2) { + // This is APK alignment field. + // FORMAT: + // * uint16 alignment multiple (in bytes) + // * remaining bytes -- padding to achieve alignment of data which starts after + // the extra field + throw new ZipException( + 'Minimum 6 bytes of the extensible data block/field used for alignment of uncompressed entries.' + ); + } + $multiple = unpack('v', $buffer)[1]; + $padding = $length - 2; + + return new self($multiple, $padding); + } + + /** + * Populate data from this array as if it was in central directory data. + * + * @param string $buffer the buffer to read data from + * @param ZipEntry|null $entry + * + * @throws ZipException on error + * + * @return ApkAlignmentExtraField + */ + public static function unpackCentralDirData($buffer, ZipEntry $entry = null) + { + return self::unpackLocalFileData($buffer, $entry); + } + + /** + * The actual data to put into local file data - without Header-ID + * or length specifier. + * + * @return string the data + */ + public function packLocalFileData() + { + return pack('vx' . $this->padding, $this->multiple); + } + + /** + * The actual data to put into central directory - without Header-ID or + * length specifier. + * + * @return string the data + */ + public function packCentralDirData() + { + return $this->packLocalFileData(); + } + + /** + * @return string + */ + public function __toString() + { + return sprintf( + '0x%04x APK Alignment: Multiple=%d Padding=%d', + self::HEADER_ID, + $this->multiple, + $this->padding + ); + } +} diff --git a/src/Model/Extra/Fields/AsiExtraField.php b/src/Model/Extra/Fields/AsiExtraField.php new file mode 100644 index 0000000..08cf014 --- /dev/null +++ b/src/Model/Extra/Fields/AsiExtraField.php @@ -0,0 +1,300 @@ +mode = (int) $mode; + $this->uid = (int) $uid; + $this->gid = (int) $gid; + $this->link = (string) $link; + } + + /** + * Returns the Header ID (type) of this Extra Field. + * The Header ID is an unsigned short integer (two bytes) + * which must be constant during the life cycle of this object. + * + * @return int + */ + public function getHeaderId() + { + return self::HEADER_ID; + } + + /** + * Populate data from this array as if it was in local file data. + * + * @param string $buffer the buffer to read data from + * @param ZipEntry|null $entry + * + * @throws Crc32Exception + * + * @return static + */ + public static function unpackLocalFileData($buffer, ZipEntry $entry = null) + { + $givenChecksum = unpack('V', $buffer)[1]; + $buffer = substr($buffer, 4); + $realChecksum = crc32($buffer); + + if ($givenChecksum !== $realChecksum) { + throw new Crc32Exception('Asi Unix Extra Filed Data', $givenChecksum, $realChecksum); + } + + $data = unpack('vmode/VlinkSize/vuid/vgid', $buffer); + $link = ''; + + if ($data['linkSize'] > 0) { + $link = substr($buffer, 8); + } + + return new self($data['mode'], $data['uid'], $data['gid'], $link); + } + + /** + * Populate data from this array as if it was in central directory data. + * + * @param string $buffer the buffer to read data from + * @param ZipEntry|null $entry + * + * @throws Crc32Exception + * + * @return AsiExtraField + */ + public static function unpackCentralDirData($buffer, ZipEntry $entry = null) + { + return self::unpackLocalFileData($buffer, $entry); + } + + /** + * The actual data to put into local file data - without Header-ID + * or length specifier. + * + * @return string the data + */ + public function packLocalFileData() + { + $data = pack( + 'vVvv', + $this->mode, + \strlen($this->link), + $this->uid, + $this->gid + ) . $this->link; + + return pack('V', crc32($data)) . $data; + } + + /** + * The actual data to put into central directory - without Header-ID or + * length specifier. + * + * @return string the data + */ + public function packCentralDirData() + { + return $this->packLocalFileData(); + } + + /** + * Name of linked file. + * + * @return string name of the file this entry links to if it is a + * symbolic link, the empty string otherwise + */ + public function getLink() + { + return $this->link; + } + + /** + * Indicate that this entry is a symbolic link to the given filename. + * + * @param string $link name of the file this entry links to, empty + * string if it is not a symbolic link + */ + public function setLink($link) + { + $this->link = $link; + $this->mode = $this->getPermissionsMode($this->mode); + } + + /** + * Is this entry a symbolic link? + * + * @return bool true if this is a symbolic link + */ + public function isLink() + { + return !empty($this->link); + } + + /** + * Get the file mode for given permissions with the correct file type. + * + * @param int $mode the mode + * + * @return int the type with the mode + */ + protected function getPermissionsMode($mode) + { + $type = UnixStat::UNX_IFMT; + + if ($this->isLink()) { + $type = UnixStat::UNX_IFLNK; + } elseif ($this->isDirectory()) { + $type = UnixStat::UNX_IFDIR; + } + + return $type | ($mode & self::PERM_MASK); + } + + /** + * Is this entry a directory? + * + * @return bool true if this entry is a directory + */ + public function isDirectory() + { + return ($this->mode & UnixStat::UNX_IFDIR) !== 0 && !$this->isLink(); + } + + /** + * @return int + */ + public function getMode() + { + return $this->mode; + } + + /** + * @param int $mode + */ + public function setMode($mode) + { + $this->mode = $this->getPermissionsMode($mode); + } + + /** + * @return int + */ + public function getUserId() + { + return $this->uid; + } + + /** + * @param int $uid + */ + public function setUserId($uid) + { + $this->uid = $uid; + } + + /** + * @return int + */ + public function getGroupId() + { + return $this->gid; + } + + /** + * @param int $gid + */ + public function setGroupId($gid) + { + $this->gid = $gid; + } + + /** + * @return string + */ + public function __toString() + { + return sprintf( + '0x%04x ASI: Mode=%o UID=%d GID=%d Link="%s', + self::HEADER_ID, + $this->mode, + $this->uid, + $this->gid, + $this->link + ); + } +} diff --git a/src/Model/Extra/Fields/ExtendedTimestampExtraField.php b/src/Model/Extra/Fields/ExtendedTimestampExtraField.php new file mode 100644 index 0000000..4019ca4 --- /dev/null +++ b/src/Model/Extra/Fields/ExtendedTimestampExtraField.php @@ -0,0 +1,425 @@ +flags = (int) $flags; + $this->modifyTime = $modifyTime; + $this->accessTime = $accessTime; + $this->createTime = $createTime; + } + + /** + * @param int|null $modifyTime + * @param int|null $accessTime + * @param int|null $createTime + * + * @return ExtendedTimestampExtraField + */ + public static function create($modifyTime, $accessTime, $createTime) + { + $flags = 0; + + if ($modifyTime !== null) { + $modifyTime = (int) $modifyTime; + $flags |= self::MODIFY_TIME_BIT; + } + + if ($accessTime !== null) { + $accessTime = (int) $accessTime; + $flags |= self::ACCESS_TIME_BIT; + } + + if ($createTime !== null) { + $createTime = (int) $createTime; + $flags |= self::CREATE_TIME_BIT; + } + + return new self($flags, $modifyTime, $accessTime, $createTime); + } + + /** + * Returns the Header ID (type) of this Extra Field. + * The Header ID is an unsigned short integer (two bytes) + * which must be constant during the life cycle of this object. + * + * @return int + */ + public function getHeaderId() + { + return self::HEADER_ID; + } + + /** + * Populate data from this array as if it was in local file data. + * + * @param string $buffer the buffer to read data from + * @param ZipEntry|null $entry + * + * @return ExtendedTimestampExtraField + */ + public static function unpackLocalFileData($buffer, ZipEntry $entry = null) + { + $length = \strlen($buffer); + $flags = unpack('C', $buffer)[1]; + $offset = 1; + + $modifyTime = null; + $accessTime = null; + $createTime = null; + + if (($flags & self::MODIFY_TIME_BIT) === self::MODIFY_TIME_BIT) { + $modifyTime = unpack('V', substr($buffer, $offset, 4))[1]; + $offset += 4; + } + + // Notice the extra length check in case we are parsing the shorter + // central data field (for both access and create timestamps). + if ((($flags & self::ACCESS_TIME_BIT) === self::ACCESS_TIME_BIT) && $offset + 4 <= $length) { + $accessTime = unpack('V', substr($buffer, $offset, 4))[1]; + $offset += 4; + } + + if ((($flags & self::CREATE_TIME_BIT) === self::CREATE_TIME_BIT) && $offset + 4 <= $length) { + $createTime = unpack('V', substr($buffer, $offset, 4))[1]; + } + + return new self($flags, $modifyTime, $accessTime, $createTime); + } + + /** + * Populate data from this array as if it was in central directory data. + * + * @param string $buffer the buffer to read data from + * @param ZipEntry|null $entry + * + * @return ExtendedTimestampExtraField + */ + public static function unpackCentralDirData($buffer, ZipEntry $entry = null) + { + return self::unpackLocalFileData($buffer, $entry); + } + + /** + * The actual data to put into local file data - without Header-ID + * or length specifier. + * + * @return string the data + */ + public function packLocalFileData() + { + $data = ''; + + if (($this->flags & self::MODIFY_TIME_BIT) === self::MODIFY_TIME_BIT && $this->modifyTime !== null) { + $data .= pack('V', $this->modifyTime); + } + + if (($this->flags & self::ACCESS_TIME_BIT) === self::ACCESS_TIME_BIT && $this->accessTime !== null) { + $data .= pack('V', $this->accessTime); + } + + if (($this->flags & self::CREATE_TIME_BIT) === self::CREATE_TIME_BIT && $this->createTime !== null) { + $data .= pack('V', $this->createTime); + } + + return pack('C', $this->flags) . $data; + } + + /** + * The actual data to put into central directory - without Header-ID or + * length specifier. + * + * Note: even if bit1 and bit2 are set, the Central data will still + * not contain access/create fields: only local data ever holds those! + * + * @return string the data + */ + public function packCentralDirData() + { + $cdLength = 1 + ($this->modifyTime !== null ? 4 : 0); + + return substr($this->packLocalFileData(), 0, $cdLength); + } + + /** + * Gets flags byte. + * + * The flags byte tells us which of the three datestamp fields are + * present in the data: + * bit0 - modify time + * bit1 - access time + * bit2 - create time + * + * Only first 3 bits of flags are used according to the + * latest version of the spec (December 2012). + * + * @return int flags byte indicating which of the + * three datestamp fields are present + */ + public function getFlags() + { + return $this->flags; + } + + /** + * Returns the modify time (seconds since epoch) of this zip entry, + * or null if no such timestamp exists in the zip entry. + * + * @return int|null modify time (seconds since epoch) or null + */ + public function getModifyTime() + { + return $this->modifyTime; + } + + /** + * Returns the access time (seconds since epoch) of this zip entry, + * or null if no such timestamp exists in the zip entry. + * + * @return int|null access time (seconds since epoch) or null + */ + public function getAccessTime() + { + return $this->accessTime; + } + + /** + * Returns the create time (seconds since epoch) of this zip entry, + * or null if no such timestamp exists in the zip entry. + * + * Note: modern linux file systems (e.g., ext2) + * do not appear to store a "create time" value, and so + * it's usually omitted altogether in the zip extra + * field. Perhaps other unix systems track this. + * + * @return int|null create time (seconds since epoch) or null + */ + public function getCreateTime() + { + return $this->createTime; + } + + /** + * Returns the modify time as a \DateTimeInterface + * of this zip entry, or null if no such timestamp exists in the zip entry. + * The milliseconds are always zeroed out, since the underlying data + * offers only per-second precision. + * + * @return \DateTimeInterface|null modify time as \DateTimeInterface or null + */ + public function getModifyDateTime() + { + return self::timestampToDateTime($this->modifyTime); + } + + /** + * Returns the access time as a \DateTimeInterface + * of this zip entry, or null if no such timestamp exists in the zip entry. + * The milliseconds are always zeroed out, since the underlying data + * offers only per-second precision. + * + * @return \DateTimeInterface|null access time as \DateTimeInterface or null + */ + public function getAccessDateTime() + { + return self::timestampToDateTime($this->accessTime); + } + + /** + * Returns the create time as a a \DateTimeInterface + * of this zip entry, or null if no such timestamp exists in the zip entry. + * The milliseconds are always zeroed out, since the underlying data + * offers only per-second precision. + * + * Note: modern linux file systems (e.g., ext2) + * do not appear to store a "create time" value, and so + * it's usually omitted altogether in the zip extra + * field. Perhaps other unix systems track $this->. + * + * @return \DateTimeInterface|null create time as \DateTimeInterface or null + */ + public function getCreateDateTime() + { + return self::timestampToDateTime($this->createTime); + } + + /** + * Sets the modify time (seconds since epoch) of this zip entry + * using a integer. + * + * @param int|null $unixTime unix time of the modify time (seconds per epoch) or null + */ + public function setModifyTime($unixTime) + { + $this->modifyTime = $unixTime; + } + + /** + * Sets the access time (seconds since epoch) of this zip entry + * using a integer. + * + * @param int|null $unixTime Unix time of the access time (seconds per epoch) or null + */ + public function setAccessTime($unixTime) + { + $this->accessTime = $unixTime; + } + + /** + * Sets the create time (seconds since epoch) of this zip entry + * using a integer. + * + * @param int|null $unixTime Unix time of the create time (seconds per epoch) or null + */ + public function setCreateTime($unixTime) + { + $this->createTime = $unixTime; + } + + /** + * @param int|null $timestamp + * + * @return \DateTimeInterface|null + */ + private static function timestampToDateTime($timestamp) + { + try { + return $timestamp !== null ? new \DateTimeImmutable('@' . $timestamp) : null; + } catch (\Exception $e) { + return null; + } + } + + /** + * @return string + */ + public function __toString() + { + $args = [self::HEADER_ID]; + $format = '0x%04x ExtendedTimestamp:'; + + if ($this->modifyTime !== null) { + $format .= ' Modify:[%s]'; + $args[] = date(\DATE_W3C, $this->modifyTime); + } + + if ($this->accessTime !== null) { + $format .= ' Access:[%s]'; + $args[] = date(\DATE_W3C, $this->accessTime); + } + + if ($this->createTime !== null) { + $format .= ' Create:[%s]'; + $args[] = date(\DATE_W3C, $this->createTime); + } + + return vsprintf($format, $args); + } +} diff --git a/src/Model/Extra/Fields/JarMarkerExtraField.php b/src/Model/Extra/Fields/JarMarkerExtraField.php new file mode 100644 index 0000000..e1683aa --- /dev/null +++ b/src/Model/Extra/Fields/JarMarkerExtraField.php @@ -0,0 +1,118 @@ +getEntries(); + + if (!empty($zipEntries)) { + foreach ($zipEntries as $zipEntry) { + $zipEntry->removeExtraField(self::HEADER_ID); + } + // set jar execute bit + reset($zipEntries); + $zipEntry = current($zipEntries); + $zipEntry->getCdExtraFields()[] = new self(); + } + } + + /** + * Returns the Header ID (type) of this Extra Field. + * The Header ID is an unsigned short integer (two bytes) + * which must be constant during the life cycle of this object. + * + * @return int + */ + public function getHeaderId() + { + return self::HEADER_ID; + } + + /** + * The actual data to put into local file data - without Header-ID + * or length specifier. + * + * @return string the data + */ + public function packLocalFileData() + { + return ''; + } + + /** + * The actual data to put into central directory - without Header-ID or + * length specifier. + * + * @return string the data + */ + public function packCentralDirData() + { + return ''; + } + + /** + * Populate data from this array as if it was in local file data. + * + * @param string $buffer the buffer to read data from + * @param ZipEntry|null $entry + * + * @throws ZipException on error + * + * @return JarMarkerExtraField + */ + public static function unpackLocalFileData($buffer, ZipEntry $entry = null) + { + if (!empty($buffer)) { + throw new ZipException("JarMarker doesn't expect any data"); + } + + return new self(); + } + + /** + * Populate data from this array as if it was in central directory data. + * + * @param string $buffer the buffer to read data from + * @param ZipEntry|null $entry + * + * @throws ZipException on error + * + * @return JarMarkerExtraField + */ + public static function unpackCentralDirData($buffer, ZipEntry $entry = null) + { + return self::unpackLocalFileData($buffer, $entry); + } + + /** + * @return string + */ + public function __toString() + { + return sprintf('0x%04x Jar Marker', self::HEADER_ID); + } +} diff --git a/src/Model/Extra/Fields/NewUnixExtraField.php b/src/Model/Extra/Fields/NewUnixExtraField.php new file mode 100644 index 0000000..2f657af --- /dev/null +++ b/src/Model/Extra/Fields/NewUnixExtraField.php @@ -0,0 +1,248 @@ +version = (int) $version; + $this->uid = (int) $uid; + $this->gid = (int) $gid; + } + + /** + * Returns the Header ID (type) of this Extra Field. + * The Header ID is an unsigned short integer (two bytes) + * which must be constant during the life cycle of this object. + * + * @return int + */ + public function getHeaderId() + { + return self::HEADER_ID; + } + + /** + * Populate data from this array as if it was in local file data. + * + * @param string $buffer the buffer to read data from + * @param ZipEntry|null $entry + * + * @throws ZipException + * + * @return NewUnixExtraField + */ + public static function unpackLocalFileData($buffer, ZipEntry $entry = null) + { + $length = \strlen($buffer); + + if ($length < 3) { + throw new ZipException(sprintf('X7875_NewUnix length is too short, only %s bytes', $length)); + } + $offset = 0; + $data = unpack('Cversion/CuidSize', $buffer); + $offset += 2; + $uidSize = $data['uidSize']; + $gid = self::readSizeIntegerLE(substr($buffer, $offset, $uidSize), $uidSize); + $offset += $uidSize; + $gidSize = unpack('C', $buffer[$offset])[1]; + $offset++; + $uid = self::readSizeIntegerLE(substr($buffer, $offset, $gidSize), $gidSize); + + return new self($data['version'], $gid, $uid); + } + + /** + * Converts a signed byte into an unsigned integer representation + * (e.g., -1 becomes 255). + * + * @param int $b byte to convert to int + * + * @return int representation of the provided byte + * + * @since 1.5 + */ + public static function signedByteToUnsignedInt($b) + { + if ($b >= 0) { + return $b; + } + + return 256 + $b; + } + + /** + * Populate data from this array as if it was in central directory data. + * + * @param string $buffer the buffer to read data from + * @param ZipEntry|null $entry + * + * @throws ZipException + * + * @return NewUnixExtraField + */ + public static function unpackCentralDirData($buffer, ZipEntry $entry = null) + { + return self::unpackLocalFileData($buffer, $entry); + } + + /** + * The actual data to put into local file data - without Header-ID + * or length specifier. + * + * @return string the data + */ + public function packLocalFileData() + { + return pack( + 'CCVCV', + $this->version, + 4, // GIDSize + $this->gid, + 4, // UIDSize + $this->uid + ); + } + + /** + * The actual data to put into central directory - without Header-ID or + * length specifier. + * + * @return string the data + */ + public function packCentralDirData() + { + return $this->packLocalFileData(); + } + + /** + * @param string $data + * @param int $size + * + * @throws ZipException + * + * @return int + */ + private static function readSizeIntegerLE($data, $size) + { + $format = [ + 1 => 'C', // unsigned byte + 2 => 'v', // unsigned short LE + 4 => 'V', // unsigned int LE + ]; + + if (!isset($format[$size])) { + throw new ZipException(sprintf('Invalid size bytes: %d', $size)); + } + + return unpack($format[$size], $data)[1]; + } + + /** + * @return int + */ + public function getUid() + { + return $this->uid; + } + + /** + * @param int $uid + */ + public function setUid($uid) + { + $this->uid = $uid & 0xffffffff; + } + + /** + * @return int + */ + public function getGid() + { + return $this->gid; + } + + /** + * @param int $gid + */ + public function setGid($gid) + { + $this->gid = $gid & 0xffffffff; + } + + /** + * @return string + */ + public function __toString() + { + return sprintf( + '0x%04x NewUnix: UID=%d GID=%d', + self::HEADER_ID, + $this->uid, + $this->gid + ); + } +} diff --git a/src/Model/Extra/Fields/NtfsExtraField.php b/src/Model/Extra/Fields/NtfsExtraField.php new file mode 100644 index 0000000..7d68d78 --- /dev/null +++ b/src/Model/Extra/Fields/NtfsExtraField.php @@ -0,0 +1,242 @@ +modifyTime = (int) $modifyTime; + $this->accessTime = (int) $accessTime; + $this->createTime = (int) $createTime; + } + + /** + * Returns the Header ID (type) of this Extra Field. + * The Header ID is an unsigned short integer (two bytes) + * which must be constant during the life cycle of this object. + * + * @return int + */ + public function getHeaderId() + { + return self::HEADER_ID; + } + + /** + * Populate data from this array as if it was in local file data. + * + * @param string $buffer the buffer to read data from + * @param ZipEntry|null $entry + * + * @return NtfsExtraField + */ + public static function unpackLocalFileData($buffer, ZipEntry $entry = null) + { + $buffer = substr($buffer, 4); + + $modifyTime = 0; + $accessTime = 0; + $createTime = 0; + + while ($buffer || $buffer !== '') { + $unpack = unpack('vtag/vsizeAttr', $buffer); + + if ($unpack['tag'] === self::TIME_ATTR_TAG && $unpack['sizeAttr'] === self::TIME_ATTR_SIZE) { + // refactoring will be needed when php 5.5 support ends + $modifyTime = PackUtil::unpackLongLE(substr($buffer, 4, 8)); + $accessTime = PackUtil::unpackLongLE(substr($buffer, 12, 8)); + $createTime = PackUtil::unpackLongLE(substr($buffer, 20, 8)); + + break; + } + $buffer = substr($buffer, 4 + $unpack['sizeAttr']); + } + + return new self($modifyTime, $accessTime, $createTime); + } + + /** + * Populate data from this array as if it was in central directory data. + * + * @param string $buffer the buffer to read data from + * @param ZipEntry|null $entry + * + * @return NtfsExtraField + */ + public static function unpackCentralDirData($buffer, ZipEntry $entry = null) + { + return self::unpackLocalFileData($buffer, $entry); + } + + /** + * The actual data to put into local file data - without Header-ID + * or length specifier. + * + * @return string the data + */ + public function packLocalFileData() + { + $data = pack('Vvv', 0, self::TIME_ATTR_TAG, self::TIME_ATTR_SIZE); + // refactoring will be needed when php 5.5 support ends + $data .= PackUtil::packLongLE($this->modifyTime); + $data .= PackUtil::packLongLE($this->accessTime); + $data .= PackUtil::packLongLE($this->createTime); + + return $data; + } + + /** + * The actual data to put into central directory - without Header-ID or + * length specifier. + * + * @return string the data + */ + public function packCentralDirData() + { + return $this->packLocalFileData(); + } + + /** + * @return \DateTimeInterface + */ + public function getModifyDateTime() + { + return $this->ntfsTimeToDateTime($this->modifyTime); + } + + /** + * @param \DateTimeInterface $modifyTime + */ + public function setModifyDateTime(\DateTimeInterface $modifyTime) + { + $this->modifyTime = $this->dateTimeToNtfsTime($modifyTime); + } + + /** + * @return \DateTimeInterface + */ + public function getAccessDateTime() + { + return $this->ntfsTimeToDateTime($this->accessTime); + } + + /** + * @param \DateTimeInterface $accessTime + */ + public function setAccessDateTime(\DateTimeInterface $accessTime) + { + $this->accessTime = $this->dateTimeToNtfsTime($accessTime); + } + + /** + * @return \DateTimeInterface + */ + public function getCreateDateTime() + { + return $this->ntfsTimeToDateTime($this->createTime); + } + + /** + * @param \DateTimeInterface $createTime + */ + public function setCreateDateTime(\DateTimeInterface $createTime) + { + $this->createTime = $this->dateTimeToNtfsTime($createTime); + } + + /** + * @param \DateTimeInterface $dateTime + * + * @return int + */ + protected function dateTimeToNtfsTime(\DateTimeInterface $dateTime) + { + return $dateTime->getTimestamp() * 10000000 + self::EPOCH_OFFSET; + } + + /** + * @param int $time + * + * @return \DateTimeInterface + */ + protected function ntfsTimeToDateTime($time) + { + $timestamp = (int) ($time / 10000000 + self::EPOCH_OFFSET); + + try { + return new \DateTimeImmutable('@' . $timestamp); + } catch (\Exception $e) { + throw new InvalidArgumentException('Cannot create date/time object for timestamp ' . $timestamp, 1, $e); + } + } + + /** + * @return string + */ + public function __toString() + { + $args = [self::HEADER_ID]; + $format = '0x%04x NtfsExtra:'; + + if ($this->modifyTime !== 0) { + $format .= ' Modify:[%s]'; + $args[] = $this->getModifyDateTime()->format(\DATE_ATOM); + } + + if ($this->accessTime !== 0) { + $format .= ' Access:[%s]'; + $args[] = $this->getAccessDateTime()->format(\DATE_ATOM); + } + + if ($this->createTime !== 0) { + $format .= ' Create:[%s]'; + $args[] = $this->getCreateDateTime()->format(\DATE_ATOM); + } + + return vsprintf($format, $args); + } +} diff --git a/src/Model/Extra/Fields/OldUnixExtraField.php b/src/Model/Extra/Fields/OldUnixExtraField.php new file mode 100644 index 0000000..1f69487 --- /dev/null +++ b/src/Model/Extra/Fields/OldUnixExtraField.php @@ -0,0 +1,327 @@ +accessTime = $accessTime; + $this->modifyTime = $modifyTime; + $this->uid = $uid; + $this->gid = $gid; + } + + /** + * Returns the Header ID (type) of this Extra Field. + * The Header ID is an unsigned short integer (two bytes) + * which must be constant during the life cycle of this object. + * + * @return int + */ + public function getHeaderId() + { + return self::HEADER_ID; + } + + /** + * Populate data from this array as if it was in local file data. + * + * @param string $buffer the buffer to read data from + * @param ZipEntry|null $entry + * + * @return OldUnixExtraField + */ + public static function unpackLocalFileData($buffer, ZipEntry $entry = null) + { + $length = \strlen($buffer); + + $accessTime = $modifyTime = $uid = $gid = null; + + if ($length >= 4) { + $accessTime = unpack('V', $buffer)[1]; + } + + if ($length >= 8) { + $modifyTime = unpack('V', substr($buffer, 4, 4))[1]; + } + + if ($length >= 10) { + $uid = unpack('v', substr($buffer, 8, 2))[1]; + } + + if ($length >= 12) { + $gid = unpack('v', substr($buffer, 10, 2))[1]; + } + + return new self($accessTime, $modifyTime, $uid, $gid); + } + + /** + * Populate data from this array as if it was in central directory data. + * + * @param string $buffer the buffer to read data from + * @param ZipEntry|null $entry + * + * @return OldUnixExtraField + */ + public static function unpackCentralDirData($buffer, ZipEntry $entry = null) + { + $length = \strlen($buffer); + + $accessTime = $modifyTime = null; + + if ($length >= 4) { + $accessTime = unpack('V', $buffer)[1]; + } + + if ($length >= 8) { + $modifyTime = unpack('V', substr($buffer, 4, 4))[1]; + } + + return new self($accessTime, $modifyTime, null, null); + } + + /** + * The actual data to put into local file data - without Header-ID + * or length specifier. + * + * @return string the data + */ + public function packLocalFileData() + { + $data = ''; + + if ($this->accessTime !== null) { + $data .= pack('V', $this->accessTime); + + if ($this->modifyTime !== null) { + $data .= pack('V', $this->modifyTime); + + if ($this->uid !== null) { + $data .= pack('v', $this->uid); + + if ($this->gid !== null) { + $data .= pack('v', $this->gid); + } + } + } + } + + return $data; + } + + /** + * The actual data to put into central directory - without Header-ID or + * length specifier. + * + * @return string the data + */ + public function packCentralDirData() + { + $data = ''; + + if ($this->accessTime !== null) { + $data .= pack('V', $this->accessTime); + + if ($this->modifyTime !== null) { + $data .= pack('V', $this->modifyTime); + } + } + + return $data; + } + + /** + * @return int|null + */ + public function getAccessTime() + { + return $this->accessTime; + } + + /** + * @param int|null $accessTime + */ + public function setAccessTime($accessTime) + { + $this->accessTime = $accessTime; + } + + /** + * @return \DateTimeInterface|null + */ + public function getAccessDateTime() + { + try { + return $this->accessTime === null ? null : + new \DateTimeImmutable('@' . $this->accessTime); + } catch (\Exception $e) { + return null; + } + } + + /** + * @return int|null + */ + public function getModifyTime() + { + return $this->modifyTime; + } + + /** + * @param int|null $modifyTime + */ + public function setModifyTime($modifyTime) + { + $this->modifyTime = $modifyTime; + } + + /** + * @return \DateTimeInterface|null + */ + public function getModifyDateTime() + { + try { + return $this->modifyTime === null ? null : + new \DateTimeImmutable('@' . $this->modifyTime); + } catch (\Exception $e) { + return null; + } + } + + /** + * @return int|null + */ + public function getUid() + { + return $this->uid; + } + + /** + * @param int|null $uid + */ + public function setUid($uid) + { + $this->uid = $uid; + } + + /** + * @return int|null + */ + public function getGid() + { + return $this->gid; + } + + /** + * @param int|null $gid + */ + public function setGid($gid) + { + $this->gid = $gid; + } + + /** + * @return string + */ + public function __toString() + { + $args = [self::HEADER_ID]; + $format = '0x%04x OldUnix:'; + + if (($modifyTime = $this->getModifyDateTime()) !== null) { + $format .= ' Modify:[%s]'; + $args[] = $modifyTime->format(\DATE_ATOM); + } + + if (($accessTime = $this->getAccessDateTime()) !== null) { + $format .= ' Access:[%s]'; + $args[] = $accessTime->format(\DATE_ATOM); + } + + if ($this->uid !== null) { + $format .= ' UID=%d'; + $args[] = $this->uid; + } + + if ($this->gid !== null) { + $format .= ' GID=%d'; + $args[] = $this->gid; + } + + return vsprintf($format, $args); + } +} diff --git a/src/Model/Extra/Fields/UnicodeCommentExtraField.php b/src/Model/Extra/Fields/UnicodeCommentExtraField.php new file mode 100644 index 0000000..d8280b6 --- /dev/null +++ b/src/Model/Extra/Fields/UnicodeCommentExtraField.php @@ -0,0 +1,76 @@ +getUnicodeValue() + ); + } +} diff --git a/src/Model/Extra/Fields/UnicodePathExtraField.php b/src/Model/Extra/Fields/UnicodePathExtraField.php new file mode 100644 index 0000000..047b2d0 --- /dev/null +++ b/src/Model/Extra/Fields/UnicodePathExtraField.php @@ -0,0 +1,77 @@ +getUnicodeValue() + ); + } +} diff --git a/src/Model/Extra/Fields/UnrecognizedExtraField.php b/src/Model/Extra/Fields/UnrecognizedExtraField.php new file mode 100644 index 0000000..d0f45f8 --- /dev/null +++ b/src/Model/Extra/Fields/UnrecognizedExtraField.php @@ -0,0 +1,115 @@ +headerId = (int) $headerId; + $this->data = (string) $data; + } + + /** + * @param int $headerId + */ + public function setHeaderId($headerId) + { + $this->headerId = $headerId; + } + + /** + * Returns the Header ID (type) of this Extra Field. + * The Header ID is an unsigned short integer (two bytes) + * which must be constant during the life cycle of this object. + * + * @return int + */ + public function getHeaderId() + { + return $this->headerId; + } + + /** + * Populate data from this array as if it was in local file data. + * + * @param string $buffer the buffer to read data from + * @param ZipEntry|null $entry + */ + public static function unpackLocalFileData($buffer, ZipEntry $entry = null) + { + throw new \RuntimeException('Unsupport parse'); + } + + /** + * Populate data from this array as if it was in central directory data. + * + * @param string $buffer the buffer to read data from + * @param ZipEntry|null $entry + */ + public static function unpackCentralDirData($buffer, ZipEntry $entry = null) + { + throw new \RuntimeException('Unsupport parse'); + } + + /** + * {@inheritdoc} + */ + public function packLocalFileData() + { + return $this->data; + } + + /** + * {@inheritdoc} + */ + public function packCentralDirData() + { + return $this->data; + } + + /** + * @return string + */ + public function getData() + { + return $this->data; + } + + /** + * @param string $data + */ + public function setData($data) + { + $this->data = (string) $data; + } + + /** + * @return string + */ + public function __toString() + { + $args = [$this->headerId, $this->data]; + $format = '0x%04x Unrecognized Extra Field: "%s"'; + + return vsprintf($format, $args); + } +} diff --git a/src/Model/Extra/Fields/WinZipAesExtraField.php b/src/Model/Extra/Fields/WinZipAesExtraField.php new file mode 100644 index 0000000..5334f09 --- /dev/null +++ b/src/Model/Extra/Fields/WinZipAesExtraField.php @@ -0,0 +1,387 @@ + */ + private static $encryptionStrengths = [ + self::KEY_STRENGTH_128BIT => 128, + self::KEY_STRENGTH_192BIT => 192, + self::KEY_STRENGTH_256BIT => 256, + ]; + + /** @var array */ + private static $MAP_KEY_STRENGTH_METHODS = [ + self::KEY_STRENGTH_128BIT => ZipEncryptionMethod::WINZIP_AES_128, + self::KEY_STRENGTH_192BIT => ZipEncryptionMethod::WINZIP_AES_192, + self::KEY_STRENGTH_256BIT => ZipEncryptionMethod::WINZIP_AES_256, + ]; + + /** @var int Integer version number specific to the zip vendor */ + private $vendorVersion = self::VERSION_AE1; + + /** @var int Integer mode value indicating AES encryption strength */ + private $keyStrength = self::KEY_STRENGTH_256BIT; + + /** @var int The actual compression method used to compress the file */ + private $compressionMethod; + + /** + * @param int $vendorVersion Integer version number specific to the zip vendor + * @param int $keyStrength Integer mode value indicating AES encryption strength + * @param int $compressionMethod The actual compression method used to compress the file + * + * @throws ZipUnsupportMethodException + */ + public function __construct($vendorVersion, $keyStrength, $compressionMethod) + { + $this->setVendorVersion($vendorVersion); + $this->setKeyStrength($keyStrength); + $this->setCompressionMethod($compressionMethod); + } + + /** + * @param ZipEntry $entry + * + * @throws ZipUnsupportMethodException + * + * @return WinZipAesExtraField + */ + public static function create(ZipEntry $entry) + { + $keyStrength = array_search($entry->getEncryptionMethod(), self::$MAP_KEY_STRENGTH_METHODS, true); + + if ($keyStrength === false) { + throw new InvalidArgumentException('Not support encryption method ' . $entry->getEncryptionMethod()); + } + + // WinZip 11 will continue to use AE-2, with no CRC, for very small files + // of less than 20 bytes. It will also use AE-2 for files compressed in + // BZIP2 format, because this format has internal integrity checks + // equivalent to a CRC check built in. + // + // https://www.winzip.com/win/en/aes_info.html + $vendorVersion = ( + $entry->getUncompressedSize() < 20 || + $entry->getCompressionMethod() === ZipCompressionMethod::BZIP2 + ) ? + self::VERSION_AE2 : + self::VERSION_AE1; + + $field = new self($vendorVersion, $keyStrength, $entry->getCompressionMethod()); + + $entry->getLocalExtraFields()->add($field); + $entry->getCdExtraFields()->add($field); + + return $field; + } + + /** + * Returns the Header ID (type) of this Extra Field. + * The Header ID is an unsigned short integer (two bytes) + * which must be constant during the life cycle of this object. + * + * @return int + */ + public function getHeaderId() + { + return self::HEADER_ID; + } + + /** + * Populate data from this array as if it was in local file data. + * + * @param string $buffer the buffer to read data from + * @param ZipEntry|null $entry + * + * @throws ZipException on error + * + * @return WinZipAesExtraField + */ + public static function unpackLocalFileData($buffer, ZipEntry $entry = null) + { + $size = \strlen($buffer); + + if ($size !== self::DATA_SIZE) { + throw new ZipException( + sprintf( + 'WinZip AES Extra data invalid size: %d. Must be %d', + $size, + self::DATA_SIZE + ) + ); + } + + $data = unpack('vvendorVersion/vvendorId/ckeyStrength/vcompressionMethod', $buffer); + + if ($data['vendorId'] !== self::VENDOR_ID) { + throw new ZipException( + sprintf( + 'Vendor id invalid: %d. Must be %d', + $data['vendorId'], + self::VENDOR_ID + ) + ); + } + + return new self( + $data['vendorVersion'], + $data['keyStrength'], + $data['compressionMethod'] + ); + } + + /** + * Populate data from this array as if it was in central directory data. + * + * @param string $buffer the buffer to read data from + * @param ZipEntry|null $entry + * + * @throws ZipException + * + * @return WinZipAesExtraField + */ + public static function unpackCentralDirData($buffer, ZipEntry $entry = null) + { + return self::unpackLocalFileData($buffer, $entry); + } + + /** + * The actual data to put into local file data - without Header-ID + * or length specifier. + * + * @return string the data + */ + public function packLocalFileData() + { + return pack( + 'vvcv', + $this->vendorVersion, + self::VENDOR_ID, + $this->keyStrength, + $this->compressionMethod + ); + } + + /** + * The actual data to put into central directory - without Header-ID or + * length specifier. + * + * @return string the data + */ + public function packCentralDirData() + { + return $this->packLocalFileData(); + } + + /** + * Returns the vendor version. + * + * @return int + * + * @see WinZipAesExtraField::VERSION_AE2 + * @see WinZipAesExtraField::VERSION_AE1 + */ + public function getVendorVersion() + { + return $this->vendorVersion; + } + + /** + * Sets the vendor version. + * + * @param int $vendorVersion the vendor version + * + * @see WinZipAesExtraField::VERSION_AE2 + * @see WinZipAesExtraField::VERSION_AE1 + */ + public function setVendorVersion($vendorVersion) + { + $vendorVersion = (int) $vendorVersion; + + if (!\in_array($vendorVersion, self::$allowVendorVersions, true)) { + throw new \InvalidArgumentException( + sprintf( + 'Unsupport WinZip AES vendor version: %d', + $vendorVersion + ) + ); + } + $this->vendorVersion = $vendorVersion; + } + + /** + * Returns vendor id. + * + * @return int + */ + public function getVendorId() + { + return self::VENDOR_ID; + } + + /** + * @return int + */ + public function getKeyStrength() + { + return $this->keyStrength; + } + + /** + * Set key strength. + * + * @param int $keyStrength + */ + public function setKeyStrength($keyStrength) + { + $keyStrength = (int) $keyStrength; + + if (!isset(self::$encryptionStrengths[$keyStrength])) { + throw new \InvalidArgumentException( + sprintf( + 'Key strength %d not support value. Allow values: %s', + $keyStrength, + implode(', ', array_keys(self::$encryptionStrengths)) + ) + ); + } + $this->keyStrength = $keyStrength; + } + + /** + * @return int + */ + public function getCompressionMethod() + { + return $this->compressionMethod; + } + + /** + * @param int $compressionMethod + * + * @throws ZipUnsupportMethodException + */ + public function setCompressionMethod($compressionMethod) + { + $compressionMethod = (int) $compressionMethod; + ZipCompressionMethod::checkSupport($compressionMethod); + $this->compressionMethod = $compressionMethod; + } + + /** + * @return int + */ + public function getEncryptionStrength() + { + return self::$encryptionStrengths[$this->keyStrength]; + } + + /** + * @return int + */ + public function getEncryptionMethod() + { + $keyStrength = $this->getKeyStrength(); + + if (!isset(self::$MAP_KEY_STRENGTH_METHODS[$keyStrength])) { + throw new InvalidArgumentException('Invalid encryption method'); + } + + return self::$MAP_KEY_STRENGTH_METHODS[$keyStrength]; + } + + /** + * @return bool + */ + public function isV1() + { + return $this->vendorVersion === self::VERSION_AE1; + } + + /** + * @return bool + */ + public function isV2() + { + return $this->vendorVersion === self::VERSION_AE2; + } + + /** + * @return int + */ + public function getSaltSize() + { + return (int) ($this->getEncryptionStrength() / 8 / 2); + } + + /** + * @return string + */ + public function __toString() + { + return sprintf( + '0x%04x WINZIP AES: VendorVersion=%d KeyStrength=0x%02x CompressionMethod=%s', + __CLASS__, + $this->vendorVersion, + $this->keyStrength, + $this->compressionMethod + ); + } +} diff --git a/src/Model/Extra/Fields/Zip64ExtraField.php b/src/Model/Extra/Fields/Zip64ExtraField.php new file mode 100644 index 0000000..4393a9c --- /dev/null +++ b/src/Model/Extra/Fields/Zip64ExtraField.php @@ -0,0 +1,311 @@ +uncompressedSize = $uncompressedSize; + $this->compressedSize = $compressedSize; + $this->localHeaderOffset = $localHeaderOffset; + $this->diskStart = $diskStart; + } + + /** + * Returns the Header ID (type) of this Extra Field. + * The Header ID is an unsigned short integer (two bytes) + * which must be constant during the life cycle of this object. + * + * @return int + */ + public function getHeaderId() + { + return self::HEADER_ID; + } + + /** + * Populate data from this array as if it was in local file data. + * + * @param string $buffer the buffer to read data from + * @param ZipEntry|null $entry + * + * @throws ZipException on error + * + * @return Zip64ExtraField + */ + public static function unpackLocalFileData($buffer, ZipEntry $entry = null) + { + $length = \strlen($buffer); + + if ($length === 0) { + // no local file data at all, may happen if an archive + // only holds a ZIP64 extended information extra field + // inside the central directory but not inside the local + // file header + return new self(); + } + + if ($length < 16) { + throw new ZipException( + 'Zip64 extended information must contain both size values in the local file header.' + ); + } + + $uncompressedSize = PackUtil::unpackLongLE(substr($buffer, 0, 8)); + $compressedSize = PackUtil::unpackLongLE(substr($buffer, 8, 8)); + + return new self($uncompressedSize, $compressedSize); + } + + /** + * Populate data from this array as if it was in central directory data. + * + * @param string $buffer the buffer to read data from + * @param ZipEntry|null $entry + * + * @throws ZipException + * + * @return Zip64ExtraField + */ + public static function unpackCentralDirData($buffer, ZipEntry $entry = null) + { + if ($entry === null) { + throw new RuntimeException('zipEntry is null'); + } + + $length = \strlen($buffer); + $remaining = $length; + + $uncompressedSize = null; + $compressedSize = null; + $localHeaderOffset = null; + $diskStart = null; + + if ($entry->getUncompressedSize() === ZipConstants::ZIP64_MAGIC) { + if ($remaining < 8) { + throw new ZipException('ZIP64 extension corrupt (no uncompressed size).'); + } + $uncompressedSize = PackUtil::unpackLongLE(substr($buffer, $length - $remaining, 8)); + $remaining -= 8; + } + + if ($entry->getCompressedSize() === ZipConstants::ZIP64_MAGIC) { + if ($remaining < 8) { + throw new ZipException('ZIP64 extension corrupt (no compressed size).'); + } + $compressedSize = PackUtil::unpackLongLE(substr($buffer, $length - $remaining, 8)); + $remaining -= 8; + } + + if ($entry->getLocalHeaderOffset() === ZipConstants::ZIP64_MAGIC) { + if ($remaining < 8) { + throw new ZipException('ZIP64 extension corrupt (no relative local header offset).'); + } + $localHeaderOffset = PackUtil::unpackLongLE(substr($buffer, $length - $remaining, 8)); + $remaining -= 8; + } + + if ($remaining === 4) { + $diskStart = unpack('V', substr($buffer, $length - $remaining, 4))[1]; + } + + return new self($uncompressedSize, $compressedSize, $localHeaderOffset, $diskStart); + } + + /** + * The actual data to put into local file data - without Header-ID + * or length specifier. + * + * @return string the data + */ + public function packLocalFileData() + { + if ($this->uncompressedSize !== null || $this->compressedSize !== null) { + if ($this->uncompressedSize === null || $this->compressedSize === null) { + throw new \InvalidArgumentException( + 'Zip64 extended information must contain both size values in the local file header.' + ); + } + + return $this->packSizes(); + } + + return ''; + } + + /** + * @return string + */ + private function packSizes() + { + $data = ''; + + if ($this->uncompressedSize !== null) { + $data .= PackUtil::packLongLE($this->uncompressedSize); + } + + if ($this->compressedSize !== null) { + $data .= PackUtil::packLongLE($this->compressedSize); + } + + return $data; + } + + /** + * The actual data to put into central directory - without Header-ID or + * length specifier. + * + * @return string the data + */ + public function packCentralDirData() + { + $data = $this->packSizes(); + + if ($this->localHeaderOffset !== null) { + $data .= PackUtil::packLongLE($this->localHeaderOffset); + } + + if ($this->diskStart !== null) { + $data .= pack('V', $this->diskStart); + } + + return $data; + } + + /** + * @return int|null + */ + public function getUncompressedSize() + { + return $this->uncompressedSize; + } + + /** + * @param int|null $uncompressedSize + */ + public function setUncompressedSize($uncompressedSize) + { + $this->uncompressedSize = $uncompressedSize; + } + + /** + * @return int|null + */ + public function getCompressedSize() + { + return $this->compressedSize; + } + + /** + * @param int|null $compressedSize + */ + public function setCompressedSize($compressedSize) + { + $this->compressedSize = $compressedSize; + } + + /** + * @return int|null + */ + public function getLocalHeaderOffset() + { + return $this->localHeaderOffset; + } + + /** + * @param int|null $localHeaderOffset + */ + public function setLocalHeaderOffset($localHeaderOffset) + { + $this->localHeaderOffset = $localHeaderOffset; + } + + /** + * @return int|null + */ + public function getDiskStart() + { + return $this->diskStart; + } + + /** + * @param int|null $diskStart + */ + public function setDiskStart($diskStart) + { + $this->diskStart = $diskStart; + } + + /** + * @return string + */ + public function __toString() + { + $args = [self::HEADER_ID]; + $format = '0x%04x ZIP64: '; + $formats = []; + + if ($this->uncompressedSize !== null) { + $formats[] = 'SIZE=%d'; + $args[] = $this->uncompressedSize; + } + + if ($this->compressedSize !== null) { + $formats[] = 'COMP_SIZE=%d'; + $args[] = $this->compressedSize; + } + + if ($this->localHeaderOffset !== null) { + $formats[] = 'OFFSET=%d'; + $args[] = $this->localHeaderOffset; + } + + if ($this->diskStart !== null) { + $formats[] = 'DISK_START=%d'; + $args[] = $this->diskStart; + } + $format .= implode(' ', $formats); + + return vsprintf($format, $args); + } +} diff --git a/src/Model/Extra/ZipExtraDriver.php b/src/Model/Extra/ZipExtraDriver.php new file mode 100644 index 0000000..e1332f0 --- /dev/null +++ b/src/Model/Extra/ZipExtraDriver.php @@ -0,0 +1,107 @@ + + * @psalm-var array> + */ + private static $implementations = [ + ApkAlignmentExtraField::HEADER_ID => ApkAlignmentExtraField::class, + AsiExtraField::HEADER_ID => AsiExtraField::class, + ExtendedTimestampExtraField::HEADER_ID => ExtendedTimestampExtraField::class, + JarMarkerExtraField::HEADER_ID => JarMarkerExtraField::class, + NewUnixExtraField::HEADER_ID => NewUnixExtraField::class, + NtfsExtraField::HEADER_ID => NtfsExtraField::class, + OldUnixExtraField::HEADER_ID => OldUnixExtraField::class, + UnicodeCommentExtraField::HEADER_ID => UnicodeCommentExtraField::class, + UnicodePathExtraField::HEADER_ID => UnicodePathExtraField::class, + WinZipAesExtraField::HEADER_ID => WinZipAesExtraField::class, + Zip64ExtraField::HEADER_ID => Zip64ExtraField::class, + ]; + + private function __construct() + { + } + + /** + * @param string|ZipExtraField $extraField ZipExtraField object or class name + */ + public static function register($extraField) + { + if (!is_a($extraField, ZipExtraField::class, true)) { + throw new InvalidArgumentException( + sprintf( + '$extraField "%s" is not implements interface %s', + (string) $extraField, + ZipExtraField::class + ) + ); + } + self::$implementations[\call_user_func([$extraField, 'getHeaderId'])] = $extraField; + } + + /** + * @param int|string|ZipExtraField $extraType ZipExtraField object or class name or extra header id + * + * @return bool + */ + public static function unregister($extraType) + { + $headerId = null; + + if (\is_int($extraType)) { + $headerId = $extraType; + } elseif (is_a($extraType, ZipExtraField::class, true)) { + $headerId = \call_user_func([$extraType, 'getHeaderId']); + } else { + return false; + } + + if (isset(self::$implementations[$headerId])) { + unset(self::$implementations[$headerId]); + + return true; + } + + return false; + } + + /** + * @param int $headerId + * + * @return string|null + */ + public static function getClassNameOrNull($headerId) + { + $headerId = (int) $headerId; + + if ($headerId < 0 || $headerId > 0xffff) { + throw new \InvalidArgumentException('$headerId out of range: ' . $headerId); + } + + if (isset(self::$implementations[$headerId])) { + return self::$implementations[$headerId]; + } + + return null; + } +} diff --git a/src/Model/Extra/ZipExtraField.php b/src/Model/Extra/ZipExtraField.php new file mode 100644 index 0000000..ce69aaf --- /dev/null +++ b/src/Model/Extra/ZipExtraField.php @@ -0,0 +1,63 @@ +entries = $entries; + $this->archiveComment = $archiveComment; + } + + /** + * @return ZipEntry[] + */ + public function &getEntries() + { + return $this->entries; + } + + /** + * @return string|null + */ + public function getArchiveComment() + { + return $this->archiveComment; + } + + /** + * Count elements of an object. + * + * @see https://php.net/manual/en/countable.count.php + * + * @return int The custom count as an integer. + * The return value is cast to an integer. + */ + public function count() + { + return \count($this->entries); + } +} diff --git a/src/Model/ZipContainer.php b/src/Model/ZipContainer.php new file mode 100644 index 0000000..1f502ad --- /dev/null +++ b/src/Model/ZipContainer.php @@ -0,0 +1,409 @@ +getEntries() as $entryName => $entry) { + $entries[$entryName] = clone $entry; + } + $archiveComment = $sourceContainer->getArchiveComment(); + } + parent::__construct($entries, $archiveComment); + $this->sourceContainer = $sourceContainer; + } + + /** + * @return ImmutableZipContainer|null + */ + public function getSourceContainer() + { + return $this->sourceContainer; + } + + /** + * @param ZipEntry $entry + */ + public function addEntry(ZipEntry $entry) + { + $this->entries[$entry->getName()] = $entry; + } + + /** + * @param string|ZipEntry $entry + * + * @return bool + */ + public function deleteEntry($entry) + { + $entry = $entry instanceof ZipEntry ? $entry->getName() : (string) $entry; + + if (isset($this->entries[$entry])) { + unset($this->entries[$entry]); + + return true; + } + + return false; + } + + /** + * @param string|ZipEntry $old + * @param string|ZipEntry $new + * + * @throws ZipException + * + * @return ZipEntry New zip entry + */ + public function renameEntry($old, $new) + { + $old = $old instanceof ZipEntry ? $old->getName() : (string) $old; + $new = $new instanceof ZipEntry ? $new->getName() : (string) $new; + + if (isset($this->entries[$new])) { + throw new InvalidArgumentException('New entry name ' . $new . ' is exists.'); + } + + $entry = $this->getEntry($old); + $newEntry = $entry->rename($new); + + $this->deleteEntry($entry); + $this->addEntry($newEntry); + + return $newEntry; + } + + /** + * @param string|ZipEntry $entryName + * + * @throws ZipEntryNotFoundException + * + * @return ZipEntry + */ + public function getEntry($entryName) + { + $entry = $this->getEntryOrNull($entryName); + + if ($entry !== null) { + return $entry; + } + + throw new ZipEntryNotFoundException($entryName); + } + + /** + * @param string|ZipEntry $entryName + * + * @return ZipEntry|null + */ + public function getEntryOrNull($entryName) + { + $entryName = $entryName instanceof ZipEntry ? $entryName->getName() : (string) $entryName; + + return isset($this->entries[$entryName]) ? $this->entries[$entryName] : null; + } + + /** + * @param string|ZipEntry $entryName + * + * @return bool + */ + public function hasEntry($entryName) + { + $entryName = $entryName instanceof ZipEntry ? $entryName->getName() : (string) $entryName; + + return isset($this->entries[$entryName]); + } + + /** + * Delete all entries. + */ + public function deleteAll() + { + $this->entries = []; + } + + /** + * Delete entries by regex pattern. + * + * @param string $regexPattern Regex pattern + * + * @return ZipEntry[] Deleted entries + */ + public function deleteByRegex($regexPattern) + { + if (empty($regexPattern)) { + throw new InvalidArgumentException('The regex pattern is not specified'); + } + + /** @var ZipEntry[] $found */ + $found = []; + + foreach ($this->entries as $entryName => $entry) { + if (preg_match($regexPattern, $entryName)) { + $found[] = $entry; + } + } + + foreach ($found as $entry) { + $this->deleteEntry($entry); + } + + return $found; + } + + /** + * Undo all changes done in the archive. + */ + public function unchangeAll() + { + $this->entries = []; + + if ($this->sourceContainer !== null) { + foreach ($this->sourceContainer->getEntries() as $entry) { + $this->entries[$entry->getName()] = clone $entry; + } + } + $this->unchangeArchiveComment(); + } + + /** + * Undo change archive comment. + */ + public function unchangeArchiveComment() + { + $this->archiveComment = null; + + if ($this->sourceContainer !== null) { + $this->archiveComment = $this->sourceContainer->archiveComment; + } + } + + /** + * Revert all changes done to an entry with the given name. + * + * @param string|ZipEntry $entry Entry name or ZipEntry + * + * @return bool + */ + public function unchangeEntry($entry) + { + $entry = $entry instanceof ZipEntry ? $entry->getName() : (string) $entry; + + if ( + $this->sourceContainer !== null && + isset($this->entries[$entry], $this->sourceContainer->entries[$entry]) + ) { + $this->entries[$entry] = clone $this->sourceContainer->entries[$entry]; + + return true; + } + + return false; + } + + /** + * Entries sort by name. + * + * Example: + * ```php + * $zipContainer->sortByName(static function (string $nameA, string $nameB): int { + * return strcmp($nameA, $nameB); + * }); + * ``` + * + * @param callable $cmp + */ + public function sortByName(callable $cmp) + { + uksort($this->entries, $cmp); + } + + /** + * Entries sort by entry. + * + * Example: + * ```php + * $zipContainer->sortByEntry(static function (ZipEntry $a, ZipEntry $b): int { + * return strcmp($a->getName(), $b->getName()); + * }); + * ``` + * + * @param callable $cmp + */ + public function sortByEntry(callable $cmp) + { + uasort($this->entries, $cmp); + } + + /** + * @param string|null $archiveComment + */ + public function setArchiveComment($archiveComment) + { + if ($archiveComment !== null && $archiveComment !== '') { + $archiveComment = (string) $archiveComment; + $length = \strlen($archiveComment); + + if ($length > 0xffff) { + throw new InvalidArgumentException('Length comment out of range'); + } + } + $this->archiveComment = $archiveComment; + } + + /** + * @param ZipEntry $entry + * + * @return bool + */ + public function hasRecompressData(ZipEntry $entry) + { + // todo test with rename, check exists ZipSourceData + if ($this->sourceContainer && isset($this->sourceContainer->entries[$entry->getName()])) { + $sourceEntry = $this->sourceContainer->entries[$entry->getName()]; + + if ( + $sourceEntry->getCompressionLevel() !== $entry->getCompressionLevel() || + $sourceEntry->getCompressionMethod() !== $entry->getCompressionMethod() || + $sourceEntry->isEncrypted() !== $entry->isEncrypted() || + $sourceEntry->getEncryptionMethod() !== $entry->getEncryptionMethod() || + $sourceEntry->getPassword() !== $entry->getPassword() || + $sourceEntry->getCompressedSize() !== $entry->getCompressedSize() || + $sourceEntry->getUncompressedSize() !== $entry->getUncompressedSize() || + $sourceEntry->getCrc() !== $entry->getCrc() + ) { + return true; + } + } + + return false; + } + + /** + * @return ZipEntryMatcher + */ + public function matcher() + { + return new ZipEntryMatcher($this); + } + + /** + * Specify a password for extracting files. + * + * @param string|null $password + */ + public function setReadPassword($password) + { + if ($this->sourceContainer !== null) { + foreach ($this->sourceContainer->entries as $entry) { + if ($entry->isEncrypted()) { + $entry->setPassword($password); + } + } + } + } + + /** + * @param string $entryName + * @param string $password + * + * @throws ZipEntryNotFoundException + * @throws ZipException + */ + public function setReadPasswordEntry($entryName, $password) + { + if (!isset($this->sourceContainer->entries[$entryName])) { + throw new ZipEntryNotFoundException($entryName); + } + + if ($this->sourceContainer->entries[$entryName]->isEncrypted()) { + $this->sourceContainer->entries[$entryName]->setPassword($password); + } + } + + /** + * @return int|null + */ + public function getZipAlign() + { + return $this->zipAlign; + } + + /** + * @param int|null $zipAlign + */ + public function setZipAlign($zipAlign) + { + $this->zipAlign = $zipAlign === null ? null : (int) $zipAlign; + } + + /** + * @return bool + */ + public function isZipAlign() + { + return $this->zipAlign !== null; + } + + /** + * @param string|null $writePassword + */ + public function setWritePassword($writePassword) + { + $this->matcher()->all()->setPassword($writePassword); + } + + /** + * Remove password. + */ + public function removePassword() + { + $this->matcher()->all()->setPassword(null); + } + + /** + * @param string|ZipEntry $entryName + */ + public function removePasswordEntry($entryName) + { + $this->matcher()->add($entryName)->setPassword(null); + } + + /** + * @param int $encryptionMethod + */ + public function setEncryptionMethod($encryptionMethod = ZipEncryptionMethod::WINZIP_AES_256) + { + $this->matcher()->all()->setEncryptionMethod($encryptionMethod); + } +} diff --git a/src/Model/ZipData.php b/src/Model/ZipData.php new file mode 100644 index 0000000..30f8289 --- /dev/null +++ b/src/Model/ZipData.php @@ -0,0 +1,28 @@ +setName($name); + + $this->cdExtraFields = new ExtraFieldsCollection(); + $this->localExtraFields = new ExtraFieldsCollection(); + } + + /** + * This method only internal use. + * + * @param string $name + * @param int $createdOS + * @param int $extractedOS + * @param int $softwareVersion + * @param int $extractVersion + * @param int $compressionMethod + * @param int $gpbf + * @param int $dosTime + * @param int $crc + * @param int $compressedSize + * @param int $uncompressedSize + * @param int $internalAttributes + * @param int $externalAttributes + * @param int $offsetLocalHeader + * @param string|null $comment + * @param string|null $charset + * + * @return ZipEntry + * + * @internal + * @noinspection PhpTooManyParametersInspection + */ + public static function create( + $name, + $createdOS, + $extractedOS, + $softwareVersion, + $extractVersion, + $compressionMethod, + $gpbf, + $dosTime, + $crc, + $compressedSize, + $uncompressedSize, + $internalAttributes, + $externalAttributes, + $offsetLocalHeader, + $comment, + $charset + ) { + $entry = new self($name); + $entry->createdOS = (int) $createdOS; + $entry->extractedOS = (int) $extractedOS; + $entry->softwareVersion = (int) $softwareVersion; + $entry->extractVersion = (int) $extractVersion; + $entry->compressionMethod = (int) $compressionMethod; + $entry->generalPurposeBitFlags = (int) $gpbf; + $entry->dosTime = (int) $dosTime; + $entry->crc = (int) $crc; + $entry->compressedSize = (int) $compressedSize; + $entry->uncompressedSize = (int) $uncompressedSize; + $entry->internalAttributes = (int) $internalAttributes; + $entry->externalAttributes = (int) $externalAttributes; + $entry->localHeaderOffset = (int) $offsetLocalHeader; + $entry->setComment($comment); + $entry->setCharset($charset); + $entry->updateCompressionLevel(); + + return $entry; + } + + /** + * Set entry name. + * + * @param string $name New entry name + * + * @return ZipEntry + */ + private function setName($name) + { + if ($name === null) { + throw new InvalidArgumentException('zip entry name is null'); + } + + $name = ltrim((string) $name, '\\/'); + + if ($name === '') { + throw new InvalidArgumentException('Empty zip entry name'); + } + + $name = (string) $name; + $length = \strlen($name); + + if ($length > 0xffff) { + throw new InvalidArgumentException('Illegal zip entry name parameter'); + } + + if (!StringUtil::isASCII($name)) { + $this->enableUtf8Name(true); + } + $this->name = $name; + $this->isDirectory = ($length = \strlen($name)) >= 1 && $name[$length - 1] === '/'; + $this->externalAttributes = $this->isDirectory ? DosAttrs::DOS_DIRECTORY : DosAttrs::DOS_ARCHIVE; + + return $this; + } + + /** + * @param string|null $charset + * + * @return ZipEntry + * + * @see DosCodePage::getCodePages() + */ + public function setCharset($charset = null) + { + if ($charset !== null && $charset === '') { + throw new InvalidArgumentException('Empty charset'); + } + $this->charset = $charset; + + return $this; + } + + /** + * @return string|null + */ + public function getCharset() + { + return $this->charset; + } + + /** + * @param string $newName New entry name + * + * @return ZipEntry new {@see ZipEntry} object with new name + */ + public function rename($newName) + { + $newEntry = clone $this; + $newEntry->setName($newName); + + $newEntry->removeExtraField(UnicodePathExtraField::HEADER_ID); + + return $newEntry; + } + + /** + * Returns the ZIP entry name. + * + * @return string + */ + public function getName() + { + return $this->name; + } + + /** + * @return ZipData|null + */ + public function getData() + { + return $this->data; + } + + /** + * @param ZipData|null $data + */ + public function setData($data) + { + $this->data = $data; + } + + /** + * @return int Get platform + * + * @deprecated Use {@see ZipEntry::getCreatedOS()} + */ + public function getPlatform() + { + @trigger_error(__METHOD__ . ' is deprecated. Use ' . __CLASS__ . '::getCreatedOS()', \E_USER_DEPRECATED); + + return $this->getCreatedOS(); + } + + /** + * @param int $platform + * + * @return ZipEntry + * + * @deprecated Use {@see ZipEntry::setCreatedOS()} + */ + public function setPlatform($platform) + { + @trigger_error(__METHOD__ . ' is deprecated. Use ' . __CLASS__ . '::setCreatedOS()', \E_USER_DEPRECATED); + + return $this->setCreatedOS($platform); + } + + /** + * @return int platform + */ + public function getCreatedOS() + { + return $this->createdOS; + } + + /** + * Set platform. + * + * @param int $platform + * + * @return ZipEntry + */ + public function setCreatedOS($platform) + { + $platform = (int) $platform; + + if ($platform < 0x00 || $platform > 0xff) { + throw new InvalidArgumentException('Platform out of range'); + } + $this->createdOS = $platform; + + return $this; + } + + /** + * @return int + */ + public function getExtractedOS() + { + return $this->extractedOS; + } + + /** + * Set extracted OS. + * + * @param int $platform + * + * @return ZipEntry + */ + public function setExtractedOS($platform) + { + $platform = (int) $platform; + + if ($platform < 0x00 || $platform > 0xff) { + throw new InvalidArgumentException('Platform out of range'); + } + $this->extractedOS = $platform; + + return $this; + } + + /** + * @return int + */ + public function getSoftwareVersion() + { + if ($this->softwareVersion === self::UNKNOWN) { + return $this->getExtractVersion(); + } + + return $this->softwareVersion; + } + + /** + * @param int $softwareVersion + * + * @return ZipEntry + */ + public function setSoftwareVersion($softwareVersion) + { + $this->softwareVersion = (int) $softwareVersion; + + return $this; + } + + /** + * Version needed to extract. + * + * @return int + * + * @deprecated Use {@see ZipEntry::getExtractVersion()} + */ + public function getVersionNeededToExtract() + { + @trigger_error(__METHOD__ . ' is deprecated. Use ' . __CLASS__ . '::getExtractVersion()', \E_USER_DEPRECATED); + + return $this->getExtractVersion(); + } + + /** + * Version needed to extract. + * + * @return int + */ + public function getExtractVersion() + { + if ($this->extractVersion === self::UNKNOWN) { + if (ZipEncryptionMethod::isWinZipAesMethod($this->encryptionMethod)) { + return ZipVersion::v51_ENCR_AES_RC2_CORRECT; + } + + if ($this->getCompressionMethod() === ZipCompressionMethod::BZIP2) { + return ZipVersion::v46_BZIP2; + } + + if ($this->isZip64ExtensionsRequired()) { + return ZipVersion::v45_ZIP64_EXT; + } + + return $this->getCompressionMethod() === ZipCompressionMethod::DEFLATED || $this->isDirectory() ? + ZipVersion::v20_DEFLATED_FOLDER_ZIPCRYPTO : + ZipVersion::v10_DEFAULT_MIN; + } + + return $this->extractVersion; + } + + /** + * Set version needed to extract. + * + * @param int $version + * + * @return ZipEntry + * + * @deprecated Use {@see ZipEntry::setExtractVersion()} + */ + public function setVersionNeededToExtract($version) + { + @trigger_error(__METHOD__ . ' is deprecated. Use ' . __CLASS__ . '::setExtractVersion()', \E_USER_DEPRECATED); + + return $this->setExtractVersion($version); + } + + /** + * Set version needed to extract. + * + * @param int $version + * + * @return ZipEntry + */ + public function setExtractVersion($version) + { + $this->extractVersion = max(ZipVersion::v10_DEFAULT_MIN, (int) $version); + + return $this; + } + + /** + * Returns the compressed size of this entry. + * + * @return int + */ + public function getCompressedSize() + { + return $this->compressedSize; + } + + /** + * Sets the compressed size of this entry. + * + * @param int $compressedSize the Compressed Size + * + * @return ZipEntry + */ + public function setCompressedSize($compressedSize) + { + $this->compressedSize = $compressedSize; + + return $this; + } + + /** + * Returns the uncompressed size of this entry. + * + * @return int + * + * @deprecated Use {@see ZipEntry::getUncompressedSize()} + */ + public function getSize() + { + @trigger_error(__METHOD__ . ' is deprecated. Use ' . __CLASS__ . '::getUncompressedSize()', \E_USER_DEPRECATED); + + return $this->getUncompressedSize(); + } + + /** + * Sets the uncompressed size of this entry. + * + * @param int $size the (Uncompressed) Size + * + * @return ZipEntry + * + * @deprecated Use {@see ZipEntry::setUncompressedSize()} + */ + public function setSize($size) + { + @trigger_error(__METHOD__ . ' is deprecated. Use ' . __CLASS__ . '::setUncompressedSize()', \E_USER_DEPRECATED); + + return $this->setUncompressedSize($size); + } + + /** + * Returns the uncompressed size of this entry. + * + * @return int + */ + public function getUncompressedSize() + { + return $this->uncompressedSize; + } + + /** + * Sets the uncompressed size of this entry. + * + * @param int $uncompressedSize the (Uncompressed) Size + * + * @return ZipEntry + */ + public function setUncompressedSize($uncompressedSize) + { + $this->uncompressedSize = $uncompressedSize; + + return $this; + } + + /** + * Return relative Offset Of Local File Header. + * + * @return int + */ + public function getLocalHeaderOffset() + { + return $this->localHeaderOffset; + } + + /** + * @param int $localHeaderOffset + * + * @return ZipEntry + */ + public function setLocalHeaderOffset($localHeaderOffset) + { + $this->localHeaderOffset = (int) $localHeaderOffset; + + return $this; + } + + /** + * Return relative Offset Of Local File Header. + * + * @return int + * + * @deprecated Use {@see ZipEntry::getLocalHeaderOffset()} + */ + public function getOffset() + { + @trigger_error( + __METHOD__ . ' is deprecated. Use ' . __CLASS__ . '::getLocalHeaderOffset()', + \E_USER_DEPRECATED + ); + + return $this->getLocalHeaderOffset(); + } + + /** + * @param int $offset + * + * @return ZipEntry + * + * @deprecated Use {@see ZipEntry::setLocalHeaderOffset()} + */ + public function setOffset($offset) + { + @trigger_error( + __METHOD__ . ' is deprecated. Use ' . __CLASS__ . '::setLocalHeaderOffset()', + \E_USER_DEPRECATED + ); + + return $this->setLocalHeaderOffset($offset); + } + + /** + * Returns the General Purpose Bit Flags. + * + * @return int + */ + public function getGeneralPurposeBitFlags() + { + return $this->generalPurposeBitFlags & 0xffff; + } + + /** + * Sets the General Purpose Bit Flags. + * + * @param mixed $general + * + * @return ZipEntry + * + * @var int general + */ + public function setGeneralPurposeBitFlags($general) + { + if ($general < 0x0000 || $general > 0xffff) { + throw new InvalidArgumentException('general out of range'); + } + $this->generalPurposeBitFlags = $general; + $this->updateCompressionLevel(); + + return $this; + } + + private function updateCompressionLevel() + { + if ($this->compressionMethod === ZipCompressionMethod::DEFLATED) { + $bit1 = $this->isSetGeneralBitFlag(GeneralPurposeBitFlag::COMPRESSION_FLAG1); + $bit2 = $this->isSetGeneralBitFlag(GeneralPurposeBitFlag::COMPRESSION_FLAG2); + + if ($bit1 && !$bit2) { + $this->compressionLevel = ZipCompressionLevel::MAXIMUM; + } elseif (!$bit1 && $bit2) { + $this->compressionLevel = ZipCompressionLevel::FAST; + } elseif ($bit1 && $bit2) { + $this->compressionLevel = ZipCompressionLevel::SUPER_FAST; + } else { + $this->compressionLevel = ZipCompressionLevel::NORMAL; + } + } + } + + /** + * @param int $mask + * @param bool $enable + * + * @return ZipEntry + */ + private function setGeneralBitFlag($mask, $enable) + { + if ($enable) { + $this->generalPurposeBitFlags |= $mask; + } else { + $this->generalPurposeBitFlags &= ~$mask; + } + + return $this; + } + + /** + * @param int $mask + * + * @return bool + */ + private function isSetGeneralBitFlag($mask) + { + return ($this->generalPurposeBitFlags & $mask) !== 0; + } + + /** + * @return bool + */ + public function isDataDescriptorEnabled() + { + return $this->isSetGeneralBitFlag(GeneralPurposeBitFlag::DATA_DESCRIPTOR); + } + + /** + * Enabling or disabling the use of the Data Descriptor block. + * + * @param bool $enabled + */ + public function enableDataDescriptor($enabled = true) + { + $this->setGeneralBitFlag(GeneralPurposeBitFlag::DATA_DESCRIPTOR, (bool) $enabled); + } + + /** + * @param bool $enabled + */ + public function enableUtf8Name($enabled) + { + $this->setGeneralBitFlag(GeneralPurposeBitFlag::UTF8, (bool) $enabled); + } + + /** + * @return bool + */ + public function isUtf8Flag() + { + return $this->isSetGeneralBitFlag(GeneralPurposeBitFlag::UTF8); + } + + /** + * Returns true if and only if this ZIP entry is encrypted. + * + * @return bool + */ + public function isEncrypted() + { + return $this->isSetGeneralBitFlag(GeneralPurposeBitFlag::ENCRYPTION); + } + + /** + * @return bool + */ + public function isStrongEncryption() + { + return $this->isSetGeneralBitFlag(GeneralPurposeBitFlag::STRONG_ENCRYPTION); + } + + /** + * Sets the encryption property to false and removes any other + * encryption artifacts. + * + * @return ZipEntry + */ + public function disableEncryption() + { + $this->setEncrypted(false); + $this->removeExtraField(WinZipAesExtraField::HEADER_ID); + $this->password = null; + + return $this; + } + + /** + * Sets the encryption flag for this ZIP entry. + * + * @param bool $encrypted + * + * @return ZipEntry + */ + private function setEncrypted($encrypted) + { + $encrypted = (bool) $encrypted; + $this->setGeneralBitFlag(GeneralPurposeBitFlag::ENCRYPTION, $encrypted); + + return $this; + } + + /** + * Returns the compression method for this entry. + * + * @return int + * + * @deprecated Use {@see ZipEntry::getCompressionMethod()} + */ + public function getMethod() + { + @trigger_error( + __METHOD__ . ' is deprecated. Use ' . __CLASS__ . '::getCompressionMethod()', + \E_USER_DEPRECATED + ); + + return $this->getCompressionMethod(); + } + + /** + * Returns the compression method for this entry. + * + * @return int + */ + public function getCompressionMethod() + { + return $this->compressionMethod; + } + + /** + * Sets the compression method for this entry. + * + * @param int $method + * + * @throws ZipUnsupportMethodException + * + * @return ZipEntry + * + * @deprecated Use {@see ZipEntry::setCompressionMethod()} + */ + public function setMethod($method) + { + @trigger_error( + __METHOD__ . ' is deprecated. Use ' . __CLASS__ . '::setCompressionMethod()', + \E_USER_DEPRECATED + ); + + return $this->setCompressionMethod($method); + } + + /** + * Sets the compression method for this entry. + * + * @param int $compressionMethod + * + * @throws ZipUnsupportMethodException + * + * @return ZipEntry + * + * @see ZipCompressionMethod::STORED + * @see ZipCompressionMethod::DEFLATED + * @see ZipCompressionMethod::BZIP2 + */ + public function setCompressionMethod($compressionMethod) + { + if (($compressionMethod < 0x0000 || $compressionMethod > 0xffff) && $compressionMethod !== self::UNKNOWN) { + throw new InvalidArgumentException('method out of range: ' . $compressionMethod); + } + + ZipCompressionMethod::checkSupport($compressionMethod); + + $this->compressionMethod = $compressionMethod; + $this->updateCompressionLevel(); + + return $this; + } + + /** + * Get Unix Timestamp. + * + * @return int + */ + public function getTime() + { + if ($this->getDosTime() === self::UNKNOWN) { + return self::UNKNOWN; + } + + return DateTimeConverter::toUnixTimestamp($this->getDosTime()); + } + + /** + * Get Dos Time. + * + * @return int + */ + public function getDosTime() + { + return $this->dosTime; + } + + /** + * Set Dos Time. + * + * @param int $dosTime + * + * @return ZipEntry + */ + public function setDosTime($dosTime) + { + $dosTime = (int) $dosTime; + + if ($dosTime < 0x00000000 || $dosTime > 0xffffffff) { + throw new InvalidArgumentException('DosTime out of range'); + } + $this->dosTime = $dosTime; + + return $this; + } + + /** + * Set time from unix timestamp. + * + * @param int $unixTimestamp + * + * @return ZipEntry + */ + public function setTime($unixTimestamp) + { + $known = $unixTimestamp !== self::UNKNOWN; + + if ($known) { + $this->dosTime = DateTimeConverter::toDosTime($unixTimestamp); + } else { + $this->dosTime = 0; + } + + return $this; + } + + /** + * Returns the external file attributes. + * + * @return int the external file attributes + */ + public function getExternalAttributes() + { + return $this->externalAttributes; + } + + /** + * Sets the external file attributes. + * + * @param int $externalAttributes the external file attributes + * + * @return ZipEntry + */ + public function setExternalAttributes($externalAttributes) + { + $this->externalAttributes = (int) $externalAttributes; + + return $this; + } + + /** + * Returns the internal file attributes. + * + * @return int the internal file attributes + */ + public function getInternalAttributes() + { + return $this->internalAttributes; + } + + /** + * Sets the internal file attributes. + * + * @param int $attributes the internal file attributes + * + * @return ZipEntry + */ + public function setInternalAttributes($attributes) + { + $this->internalAttributes = (int) $attributes; + + return $this; + } + + /** + * Returns true if and only if this ZIP entry represents a directory entry + * (i.e. end with '/'). + * + * @return bool + */ + final public function isDirectory() + { + return $this->isDirectory; + } + + /** + * @return ExtraFieldsCollection + */ + public function getCdExtraFields() + { + return $this->cdExtraFields; + } + + /** + * @param int $headerId + * + * @return ZipExtraField|null + */ + public function getCdExtraField($headerId) + { + return $this->cdExtraFields->get((int) $headerId); + } + + /** + * @param ExtraFieldsCollection $cdExtraFields + * + * @return ZipEntry + */ + public function setCdExtraFields(ExtraFieldsCollection $cdExtraFields) + { + $this->cdExtraFields = $cdExtraFields; + + return $this; + } + + /** + * @return ExtraFieldsCollection + */ + public function getLocalExtraFields() + { + return $this->localExtraFields; + } + + /** + * @param int $headerId + * + * @return ZipExtraField|null + */ + public function getLocalExtraField($headerId) + { + return $this->localExtraFields[(int) $headerId]; + } + + /** + * @param ExtraFieldsCollection $localExtraFields + * + * @return ZipEntry + */ + public function setLocalExtraFields(ExtraFieldsCollection $localExtraFields) + { + $this->localExtraFields = $localExtraFields; + + return $this; + } + + /** + * @param int $headerId + * + * @return ZipExtraField|null + */ + public function getExtraField($headerId) + { + $headerId = (int) $headerId; + $local = $this->getLocalExtraField($headerId); + + if ($local === null) { + return $this->getCdExtraField($headerId); + } + + return $local; + } + + /** + * @param int $headerId + * + * @return bool + */ + public function hasExtraField($headerId) + { + $headerId = (int) $headerId; + + return + isset($this->localExtraFields[$headerId]) || + isset($this->cdExtraFields[$headerId]); + } + + /** + * @param int $headerId + */ + public function removeExtraField($headerId) + { + $headerId = (int) $headerId; + + $this->cdExtraFields->remove($headerId); + $this->localExtraFields->remove($headerId); + } + + /** + * Returns comment entry. + * + * @return string + */ + public function getComment() + { + return $this->comment !== null ? $this->comment : ''; + } + + /** + * Set entry comment. + * + * @param string|null $comment + * + * @return ZipEntry + */ + public function setComment($comment) + { + if ($comment !== null) { + $commentLength = \strlen($comment); + + if ($commentLength < 0x0000 || $commentLength > 0xffff) { + throw new InvalidArgumentException('Comment too long'); + } + + if (!StringUtil::isASCII($comment)) { + $this->enableUtf8Name(true); + } + } + $this->comment = $comment; + + return $this; + } + + /** + * @return bool + */ + public function isDataDescriptorRequired() + { + return ($this->getCrc() | $this->getCompressedSize() | $this->getUncompressedSize()) === self::UNKNOWN; + } + + /** + * Return crc32 content or 0 for WinZip AES v2. + * + * @return int + */ + public function getCrc() + { + return $this->crc; + } + + /** + * Set crc32 content. + * + * @param int $crc + * + * @return ZipEntry + */ + public function setCrc($crc) + { + $this->crc = (int) $crc; + + return $this; + } + + /** + * @return string|null + */ + public function getPassword() + { + return $this->password; + } + + /** + * Set password and encryption method from entry. + * + * @param string|null $password + * @param int|null $encryptionMethod + * + * @return ZipEntry + */ + public function setPassword($password, $encryptionMethod = null) + { + if (!$this->isDirectory) { + if ($encryptionMethod !== null) { + $this->setEncryptionMethod($encryptionMethod); + } + + if ($password === null || $password === '') { + $this->password = null; + $this->disableEncryption(); + } else { + $this->password = (string) $password; + $this->setEncrypted(true); + } + } + + return $this; + } + + /** + * @return int + */ + public function getEncryptionMethod() + { + return $this->encryptionMethod; + } + + /** + * Set encryption method. + * + * @param int|null $encryptionMethod + * + * @return ZipEntry + * + * @see ZipEncryptionMethod::NONE + * @see ZipEncryptionMethod::PKWARE + * @see ZipEncryptionMethod::WINZIP_AES_256 + * @see ZipEncryptionMethod::WINZIP_AES_192 + * @see ZipEncryptionMethod::WINZIP_AES_128 + */ + public function setEncryptionMethod($encryptionMethod) + { + if ($encryptionMethod === null) { + $encryptionMethod = ZipEncryptionMethod::NONE; + } + + $encryptionMethod = (int) $encryptionMethod; + ZipEncryptionMethod::checkSupport($encryptionMethod); + $this->encryptionMethod = $encryptionMethod; + + $this->setEncrypted($this->encryptionMethod !== ZipEncryptionMethod::NONE); + + return $this; + } + + /** + * @return int + */ + public function getCompressionLevel() + { + return $this->compressionLevel; + } + + /** + * @param int $compressionLevel + * + * @return ZipEntry + */ + public function setCompressionLevel($compressionLevel) + { + $compressionLevel = (int) $compressionLevel; + + if ($compressionLevel === self::UNKNOWN) { + $compressionLevel = ZipCompressionLevel::NORMAL; + } + + if ( + $compressionLevel < ZipCompressionLevel::LEVEL_MIN || + $compressionLevel > ZipCompressionLevel::LEVEL_MAX + ) { + throw new InvalidArgumentException( + 'Invalid compression level. Minimum level ' . + ZipCompressionLevel::LEVEL_MIN . '. Maximum level ' . ZipCompressionLevel::LEVEL_MAX + ); + } + $this->compressionLevel = $compressionLevel; + + $this->updateGbpfCompLevel(); + + return $this; + } + + /** + * Update general purpose bit flogs. + */ + private function updateGbpfCompLevel() + { + if ($this->compressionMethod === ZipCompressionMethod::DEFLATED) { + $bit1 = false; + $bit2 = false; + + switch ($this->compressionLevel) { + case ZipCompressionLevel::MAXIMUM: + $bit1 = true; + break; + + case ZipCompressionLevel::FAST: + $bit2 = true; + break; + + case ZipCompressionLevel::SUPER_FAST: + $bit1 = true; + $bit2 = true; + break; + // default is ZipCompressionLevel::NORMAL + } + + $this->generalPurposeBitFlags |= ($bit1 ? GeneralPurposeBitFlag::COMPRESSION_FLAG1 : 0); + $this->generalPurposeBitFlags |= ($bit2 ? GeneralPurposeBitFlag::COMPRESSION_FLAG2 : 0); + } + } + + /** + * Sets Unix permissions in a way that is understood by Info-Zip's + * unzip command. + * + * @param int $mode mode an int value + * + * @return ZipEntry + */ + public function setUnixMode($mode) + { + $mode = (int) $mode; + $this->setExternalAttributes( + ($mode << 16) + // MS-DOS read-only attribute + | (($mode & UnixStat::UNX_IWUSR) === 0 ? DosAttrs::DOS_HIDDEN : 0) + // MS-DOS directory flag + | ($this->isDirectory() ? DosAttrs::DOS_DIRECTORY : DosAttrs::DOS_ARCHIVE) + ); + $this->createdOS = ZipPlatform::OS_UNIX; + + return $this; + } + + /** + * Unix permission. + * + * @return int the unix permissions + */ + public function getUnixMode() + { + /** @var AsiExtraField|null $asiExtraField */ + $asiExtraField = $this->getExtraField(AsiExtraField::HEADER_ID); + + if ($asiExtraField !== null) { + return $asiExtraField->getMode(); + } + + if ($this->createdOS === ZipPlatform::OS_UNIX) { + return ($this->externalAttributes >> 16) & 0xFFFF; + } + + return $this->isDirectory ? 040755 : 0100664; + } + + /** + * Offset MUST be considered in decision about ZIP64 format - see + * description of Data Descriptor in ZIP File Format Specification. + * + * @return bool + */ + public function isZip64ExtensionsRequired() + { + return $this->compressedSize >= ZipConstants::ZIP64_MAGIC + || $this->uncompressedSize >= ZipConstants::ZIP64_MAGIC; + } + + /** + * Returns true if this entry represents a unix symlink, + * in which case the entry's content contains the target path + * for the symlink. + * + * @return bool true if the entry represents a unix symlink, + * false otherwise + */ + public function isUnixSymlink() + { + return ($this->getUnixMode() & UnixStat::UNX_IFMT) === UnixStat::UNX_IFLNK; + } + + /** + * @return \DateTimeInterface + */ + public function getMTime() + { + /** @var NtfsExtraField|null $ntfsExtra */ + $ntfsExtra = $this->getExtraField(NtfsExtraField::HEADER_ID); + + if ($ntfsExtra !== null) { + return $ntfsExtra->getModifyDateTime(); + } + + /** @var ExtendedTimestampExtraField|null $extendedExtra */ + $extendedExtra = $this->getExtraField(ExtendedTimestampExtraField::HEADER_ID); + + if ($extendedExtra !== null && ($mtime = $extendedExtra->getModifyDateTime()) !== null) { + return $mtime; + } + + /** @var OldUnixExtraField|null $oldUnixExtra */ + $oldUnixExtra = $this->getExtraField(OldUnixExtraField::HEADER_ID); + + if ($oldUnixExtra !== null && ($mtime = $oldUnixExtra->getModifyDateTime()) !== null) { + return $mtime; + } + + $timestamp = $this->getTime(); + + try { + return new \DateTimeImmutable('@' . $timestamp); + } catch (\Exception $e) { + throw new RuntimeException('Error create DateTime object with timestamp ' . $timestamp, 1, $e); + } + } + + /** + * @return \DateTimeInterface|null + */ + public function getATime() + { + /** @var NtfsExtraField|null $ntfsExtra */ + $ntfsExtra = $this->getExtraField(NtfsExtraField::HEADER_ID); + + if ($ntfsExtra !== null) { + return $ntfsExtra->getAccessDateTime(); + } + + /** @var ExtendedTimestampExtraField|null $extendedExtra */ + $extendedExtra = $this->getExtraField(ExtendedTimestampExtraField::HEADER_ID); + + if ($extendedExtra !== null && ($atime = $extendedExtra->getAccessDateTime()) !== null) { + return $atime; + } + + /** @var OldUnixExtraField|null $oldUnixExtra */ + $oldUnixExtra = $this->getExtraField(OldUnixExtraField::HEADER_ID); + + if ($oldUnixExtra !== null) { + return $oldUnixExtra->getAccessDateTime(); + } + + return null; + } + + /** + * @return \DateTimeInterface|null + */ + public function getCTime() + { + /** @var NtfsExtraField|null $ntfsExtra */ + $ntfsExtra = $this->getExtraField(NtfsExtraField::HEADER_ID); + + if ($ntfsExtra !== null) { + return $ntfsExtra->getCreateDateTime(); + } + + /** @var ExtendedTimestampExtraField|null $extendedExtra */ + $extendedExtra = $this->getExtraField(ExtendedTimestampExtraField::HEADER_ID); + + if ($extendedExtra !== null) { + return $extendedExtra->getCreateDateTime(); + } + + return null; + } + + public function __clone() + { + $this->cdExtraFields = clone $this->cdExtraFields; + $this->localExtraFields = clone $this->localExtraFields; + + if ($this->data !== null) { + $this->data = clone $this->data; + } + } +} diff --git a/src/PhpZip/Model/ZipEntryMatcher.php b/src/Model/ZipEntryMatcher.php similarity index 69% rename from src/PhpZip/Model/ZipEntryMatcher.php rename to src/Model/ZipEntryMatcher.php index 4ff484f..1101d6e 100644 --- a/src/PhpZip/Model/ZipEntryMatcher.php +++ b/src/Model/ZipEntryMatcher.php @@ -8,8 +8,8 @@ namespace PhpZip\Model; */ class ZipEntryMatcher implements \Countable { - /** @var ZipModel */ - protected $zipModel; + /** @var ZipContainer */ + protected $zipContainer; /** @var array */ protected $matches = []; @@ -17,15 +17,15 @@ class ZipEntryMatcher implements \Countable /** * ZipEntryMatcher constructor. * - * @param ZipModel $zipModel + * @param ZipContainer $zipContainer */ - public function __construct(ZipModel $zipModel) + public function __construct(ZipContainer $zipContainer) { - $this->zipModel = $zipModel; + $this->zipContainer = $zipContainer; } /** - * @param string|array $entries + * @param string|ZipEntry|string[]|ZipEntry[] $entries * * @return ZipEntryMatcher */ @@ -46,7 +46,7 @@ class ZipEntryMatcher implements \Countable $this->matches, array_keys( array_intersect_key( - $this->zipModel->getEntries(), + $this->zipContainer->getEntries(), array_flip($entries) ) ) @@ -62,16 +62,18 @@ class ZipEntryMatcher implements \Countable * @param string $regexp * * @return ZipEntryMatcher + * + * @noinspection PhpUnusedParameterInspection */ public function match($regexp) { array_walk( - $this->zipModel->getEntries(), - function ( - /** @noinspection PhpUnusedParameterInspection */ - $entry, - $entryName - ) use ($regexp) { + $this->zipContainer->getEntries(), + /** + * @param ZipEntry $entry + * @param string $entryName + */ + function (ZipEntry $entry, $entryName) use ($regexp) { if (preg_match($regexp, $entryName)) { $this->matches[] = (string) $entryName; } @@ -87,7 +89,7 @@ class ZipEntryMatcher implements \Countable */ public function all() { - $this->matches = array_keys($this->zipModel->getEntries()); + $this->matches = array_keys($this->zipContainer->getEntries()); return $this; } @@ -105,6 +107,7 @@ class ZipEntryMatcher implements \Countable if (!empty($this->matches)) { array_walk( $this->matches, + /** @param string $entryName */ static function ($entryName) use ($callable) { $callable($entryName); } @@ -124,8 +127,9 @@ class ZipEntryMatcher implements \Countable { array_walk( $this->matches, - function ($entry) { - $this->zipModel->deleteEntry($entry); + /** @param string $entryName */ + function ($entryName) { + $this->zipContainer->deleteEntry($entryName); } ); $this->matches = []; @@ -139,11 +143,12 @@ class ZipEntryMatcher implements \Countable { array_walk( $this->matches, - function ($entry) use ($password, $encryptionMethod) { - $entry = $this->zipModel->getEntry($entry); + /** @param string $entryName */ + function ($entryName) use ($password, $encryptionMethod) { + $entry = $this->zipContainer->getEntry($entryName); if (!$entry->isDirectory()) { - $this->zipModel->getEntryForChanges($entry)->setPassword($password, $encryptionMethod); + $entry->setPassword($password, $encryptionMethod); } } ); @@ -156,11 +161,12 @@ class ZipEntryMatcher implements \Countable { array_walk( $this->matches, - function ($entry) use ($encryptionMethod) { - $entry = $this->zipModel->getEntry($entry); + /** @param string $entryName */ + function ($entryName) use ($encryptionMethod) { + $entry = $this->zipContainer->getEntry($entryName); if (!$entry->isDirectory()) { - $this->zipModel->getEntryForChanges($entry)->setEncryptionMethod($encryptionMethod); + $entry->setEncryptionMethod($encryptionMethod); } } ); @@ -170,11 +176,11 @@ class ZipEntryMatcher implements \Countable { array_walk( $this->matches, - function ($entry) { - $entry = $this->zipModel->getEntry($entry); + /** @param string $entryName */ + function ($entryName) { + $entry = $this->zipContainer->getEntry($entryName); if (!$entry->isDirectory()) { - $entry = $this->zipModel->getEntryForChanges($entry); $entry->disableEncryption(); } } diff --git a/src/Model/ZipInfo.php b/src/Model/ZipInfo.php new file mode 100644 index 0000000..42eebbf --- /dev/null +++ b/src/Model/ZipInfo.php @@ -0,0 +1,266 @@ +entry = $entry; + } + + /** + * @param ZipEntry $entry + * + * @return string + * + * @deprecated Use {@see ZipPlatform::getPlatformName()} + */ + public static function getPlatformName(ZipEntry $entry) + { + return ZipPlatform::getPlatformName($entry->getExtractedOS()); + } + + /** + * @return string + */ + public function getName() + { + return $this->entry->getName(); + } + + /** + * @return bool + */ + public function isFolder() + { + return $this->entry->isDirectory(); + } + + /** + * @return int + */ + public function getSize() + { + return $this->entry->getUncompressedSize(); + } + + /** + * @return int + */ + public function getCompressedSize() + { + return $this->entry->getCompressedSize(); + } + + /** + * @return int + */ + public function getMtime() + { + return $this->entry->getMTime()->getTimestamp(); + } + + /** + * @return int|null + */ + public function getCtime() + { + $ctime = $this->entry->getCTime(); + + return $ctime === null ? null : $ctime->getTimestamp(); + } + + /** + * @return int|null + */ + public function getAtime() + { + $atime = $this->entry->getATime(); + + return $atime === null ? null : $atime->getTimestamp(); + } + + /** + * @return string + */ + public function getAttributes() + { + $externalAttributes = $this->entry->getExternalAttributes(); + + if ($this->entry->getCreatedOS() === ZipPlatform::OS_UNIX) { + $permission = (($externalAttributes >> 16) & 0xFFFF); + + return FileAttribUtil::getUnixMode($permission); + } + + return FileAttribUtil::getDosMode($externalAttributes); + } + + /** + * @return bool + */ + public function isEncrypted() + { + return $this->entry->isEncrypted(); + } + + /** + * @return string|null + */ + public function getComment() + { + return $this->entry->getComment(); + } + + /** + * @return int + */ + public function getCrc() + { + return $this->entry->getCrc(); + } + + /** + * @return string + * + * @deprecated use \PhpZip\Model\ZipInfo::getMethodName() + */ + public function getMethod() + { + return $this->getMethodName(); + } + + /** + * @return string + */ + public function getMethodName() + { + return ZipCompressionMethod::getCompressionMethodName($this->entry->getCompressionMethod()); + } + + /** + * @return string + */ + public function getEncryptionMethodName() + { + return ZipEncryptionMethod::getEncryptionMethodName($this->entry->getEncryptionMethod()); + } + + /** + * @return string + */ + public function getPlatform() + { + return ZipPlatform::getPlatformName($this->entry->getExtractedOS()); + } + + /** + * @return int + */ + public function getVersion() + { + return $this->entry->getExtractVersion(); + } + + /** + * @return int|null + */ + public function getEncryptionMethod() + { + $encryptionMethod = $this->entry->getEncryptionMethod(); + + return $encryptionMethod === ZipEncryptionMethod::NONE ? null : $encryptionMethod; + } + + /** + * @return int|null + */ + public function getCompressionLevel() + { + return $this->entry->getCompressionLevel(); + } + + /** + * @return int + */ + public function getCompressionMethod() + { + return $this->entry->getCompressionMethod(); + } + + /** + * @return array + */ + public function toArray() + { + return [ + 'name' => $this->getName(), + 'folder' => $this->isFolder(), + 'size' => $this->getSize(), + 'compressed_size' => $this->getCompressedSize(), + 'modified' => $this->getMtime(), + 'created' => $this->getCtime(), + 'accessed' => $this->getAtime(), + 'attributes' => $this->getAttributes(), + 'encrypted' => $this->isEncrypted(), + 'encryption_method' => $this->getEncryptionMethod(), + 'encryption_method_name' => $this->getEncryptionMethodName(), + 'comment' => $this->getComment(), + 'crc' => $this->getCrc(), + 'method_name' => $this->getMethodName(), + 'compression_method' => $this->getCompressionMethod(), + 'platform' => $this->getPlatform(), + 'version' => $this->getVersion(), + ]; + } + + /** + * @return string + */ + public function __toString() + { + $ctime = $this->entry->getCTime(); + $atime = $this->entry->getATime(); + $comment = $this->getComment(); + + return __CLASS__ . ' {' + . 'Name="' . $this->getName() . '", ' + . ($this->isFolder() ? 'Folder, ' : '') + . 'Size="' . FilesUtil::humanSize($this->getSize()) . '"' + . ', Compressed size="' . FilesUtil::humanSize($this->getCompressedSize()) . '"' + . ', Modified time="' . $this->entry->getMTime()->format(\DATE_W3C) . '", ' + . ($ctime !== null ? 'Created time="' . $ctime->format(\DATE_W3C) . '", ' : '') + . ($atime !== null ? 'Accessed time="' . $atime->format(\DATE_W3C) . '", ' : '') + . ($this->isEncrypted() ? 'Encrypted, ' : '') + . ($comment !== null ? 'Comment="' . $comment . '", ' : '') + . (!empty($this->crc) ? 'Crc=0x' . dechex($this->crc) . ', ' : '') + . 'Method name="' . $this->getMethodName() . '", ' + . 'Attributes="' . $this->getAttributes() . '", ' + . 'Platform="' . $this->getPlatform() . '", ' + . 'Version=' . $this->getVersion() + . '}'; + } +} diff --git a/src/PhpZip/Crypto/WinZipAesEngine.php b/src/PhpZip/Crypto/WinZipAesEngine.php deleted file mode 100644 index 97d8e44..0000000 --- a/src/PhpZip/Crypto/WinZipAesEngine.php +++ /dev/null @@ -1,295 +0,0 @@ -entry = $entry; - } - - /** - * Decrypt from stream resource. - * - * @param string $content Input stream buffer - * - * @throws ZipException - * @throws ZipAuthenticationException - * @throws ZipCryptoException - * - * @return string - */ - public function decrypt($content) - { - $extraFieldsCollection = $this->entry->getExtraFieldsCollection(); - - if (!isset($extraFieldsCollection[WinZipAesEntryExtraField::getHeaderId()])) { - throw new ZipCryptoException($this->entry->getName() . ' (missing extra field for WinZip AES entry)'); - } - - /** - * @var WinZipAesEntryExtraField $field - */ - $field = $extraFieldsCollection[WinZipAesEntryExtraField::getHeaderId()]; - - // Get key strength. - $keyStrengthBits = $field->getKeyStrength(); - $keyStrengthBytes = $keyStrengthBits / 8; - - $pos = $keyStrengthBytes / 2; - $salt = substr($content, 0, $pos); - $passwordVerifier = substr($content, $pos, self::PWD_VERIFIER_BITS / 8); - $pos += self::PWD_VERIFIER_BITS / 8; - - $sha1Size = 20; - - // Init start, end and size of encrypted data. - $start = $pos; - $endPos = \strlen($content); - $footerSize = $sha1Size / 2; - $end = $endPos - $footerSize; - $size = $end - $start; - - if ($size < 0) { - throw new ZipCryptoException($this->entry->getName() . ' (false positive WinZip AES entry is too short)'); - } - - // Load authentication code. - $authenticationCode = substr($content, $end, $footerSize); - - if ($end + $footerSize !== $endPos) { - // This should never happen unless someone is writing to the - // end of the file concurrently! - throw new ZipCryptoException('Expected end of file after WinZip AES authentication code!'); - } - - $password = $this->entry->getPassword(); - - if ($password === null) { - throw new ZipException(sprintf('Password not set for entry %s', $this->entry->getName())); - } - - /** - * WinZip 99-character limit. - * - * @see https://sourceforge.net/p/p7zip/discussion/383044/thread/c859a2f0/ - */ - $password = substr($password, 0, 99); - $ctrIvSize = self::AES_BLOCK_SIZE_BITS / 8; - $iv = str_repeat(\chr(0), $ctrIvSize); - - do { - // Here comes the strange part about WinZip AES encryption: - // Its unorthodox use of the Password-Based Key Derivation - // Function 2 (PBKDF2) of PKCS #5 V2.0 alias RFC 2898. - // Yes, the password verifier is only a 16 bit value. - // So we must use the MAC for password verification, too. - $keyParam = hash_pbkdf2( - 'sha1', - $password, - $salt, - self::ITERATION_COUNT, - (2 * $keyStrengthBits + self::PWD_VERIFIER_BITS) / 8, - true - ); - $key = substr($keyParam, 0, $keyStrengthBytes); - $sha1MacParam = substr($keyParam, $keyStrengthBytes, $keyStrengthBytes); - // Verify password. - } while (!$passwordVerifier === substr($keyParam, 2 * $keyStrengthBytes)); - - $content = substr($content, $start, $size); - $mac = hash_hmac('sha1', $content, $sha1MacParam, true); - - if (strpos($mac, $authenticationCode) !== 0) { - throw new ZipAuthenticationException( - $this->entry->getName() . - ' (authenticated WinZip AES entry content has been tampered with)' - ); - } - - return self::aesCtrSegmentIntegerCounter($content, $key, $iv, false); - } - - /** - * Decryption or encryption AES-CTR with Segment Integer Count (SIC). - * - * @param string $str Data - * @param string $key Key - * @param string $iv IV - * @param bool $encrypted If true encryption else decryption - * - * @return string - */ - private static function aesCtrSegmentIntegerCounter($str, $key, $iv, $encrypted = true) - { - $numOfBlocks = ceil(\strlen($str) / 16); - $ctrStr = ''; - for ($i = 0; $i < $numOfBlocks; ++$i) { - for ($j = 0; $j < 16; ++$j) { - $n = \ord($iv[$j]); - - if (++$n === 0x100) { - // overflow, set this one to 0, increment next - $iv[$j] = \chr(0); - } else { - // no overflow, just write incremented number back and abort - $iv[$j] = \chr($n); - break; - } - } - $data = substr($str, $i * 16, 16); - $ctrStr .= $encrypted ? - self::encryptCtr($data, $key, $iv) : - self::decryptCtr($data, $key, $iv); - } - - return $ctrStr; - } - - /** - * Encrypt AES-CTR. - * - * @param string $data Raw data - * @param string $key Aes key - * @param string $iv Aes IV - * - * @return string Encrypted data - */ - private static function encryptCtr($data, $key, $iv) - { - if (\extension_loaded('openssl')) { - $numBits = \strlen($key) * 8; - /** @noinspection PhpComposerExtensionStubsInspection */ - return openssl_encrypt($data, 'AES-' . $numBits . '-CTR', $key, \OPENSSL_RAW_DATA, $iv); - } - - if (\extension_loaded('mcrypt')) { - /** @noinspection PhpComposerExtensionStubsInspection */ - return mcrypt_encrypt(MCRYPT_RIJNDAEL_128, $key, $data, 'ctr', $iv); - } - - throw new RuntimeException('Extension openssl or mcrypt not loaded'); - } - - /** - * Decrypt AES-CTR. - * - * @param string $data Encrypted data - * @param string $key Aes key - * @param string $iv Aes IV - * - * @return string Raw data - */ - private static function decryptCtr($data, $key, $iv) - { - if (\extension_loaded('openssl')) { - $numBits = \strlen($key) * 8; - /** @noinspection PhpComposerExtensionStubsInspection */ - return openssl_decrypt($data, 'AES-' . $numBits . '-CTR', $key, \OPENSSL_RAW_DATA, $iv); - } - - if (\extension_loaded('mcrypt')) { - /** @noinspection PhpComposerExtensionStubsInspection */ - return mcrypt_decrypt(MCRYPT_RIJNDAEL_128, $key, $data, 'ctr', $iv); - } - - throw new RuntimeException('Extension openssl or mcrypt not loaded'); - } - - /** - * Encryption string. - * - * @param string $content - * - * @throws ZipException - * - * @return string - */ - public function encrypt($content) - { - // Init key strength. - $password = $this->entry->getPassword(); - - if ($password === null) { - throw new ZipException('No password was set for the entry "' . $this->entry->getName() . '"'); - } - - /** - * WinZip 99-character limit. - * - * @see https://sourceforge.net/p/p7zip/discussion/383044/thread/c859a2f0/ - */ - $password = substr($password, 0, 99); - - $keyStrengthBits = WinZipAesEntryExtraField::getKeyStrangeFromEncryptionMethod( - $this->entry->getEncryptionMethod() - ); - $keyStrengthBytes = $keyStrengthBits / 8; - - try { - $salt = random_bytes($keyStrengthBytes / 2); - } catch (\Exception $e) { - throw new \RuntimeException('Oops, our server is bust and cannot generate any random data.', 1, $e); - } - - $keyParam = hash_pbkdf2( - 'sha1', - $password, - $salt, - self::ITERATION_COUNT, - (2 * $keyStrengthBits + self::PWD_VERIFIER_BITS) / 8, - true - ); - $sha1HMacParam = substr($keyParam, $keyStrengthBytes, $keyStrengthBytes); - - // Can you believe they "forgot" the nonce in the CTR mode IV?! :-( - $ctrIvSize = self::AES_BLOCK_SIZE_BITS / 8; - $iv = str_repeat(\chr(0), $ctrIvSize); - - $key = substr($keyParam, 0, $keyStrengthBytes); - - $content = self::aesCtrSegmentIntegerCounter($content, $key, $iv, true); - - $mac = hash_hmac('sha1', $content, $sha1HMacParam, true); - - return $salt . - substr($keyParam, 2 * $keyStrengthBytes, self::PWD_VERIFIER_BITS / 8) . - $content . - substr($mac, 0, 10); - } -} diff --git a/src/PhpZip/Crypto/ZipEncryptionEngine.php b/src/PhpZip/Crypto/ZipEncryptionEngine.php deleted file mode 100644 index 55dbe10..0000000 --- a/src/PhpZip/Crypto/ZipEncryptionEngine.php +++ /dev/null @@ -1,36 +0,0 @@ - 0xffff) { - throw new ZipException('Extra Fields too large: ' . $extraLength); - } - $pos = 0; - $endPos = $extraLength; - - while ($endPos - $pos >= 4) { - $unpack = unpack('vheaderId/vdataSize', substr($extra, $pos, 4)); - $pos += 4; - $headerId = (int) $unpack['headerId']; - $dataSize = (int) $unpack['dataSize']; - $extraField = self::create($headerId); - - if ($extraField instanceof Zip64ExtraField && $entry !== null) { - $extraField->setEntry($entry); - } - - if ($dataSize > 0) { - $content = substr($extra, $pos, $dataSize); - - if ($content !== false) { - $extraField->deserialize($content); - $extraFieldsCollection[$headerId] = $extraField; - } - } - - $pos += $dataSize; - } - } - - return $extraFieldsCollection; - } - - /** - * @param ExtraFieldsCollection $extraFieldsCollection - * - * @throws ZipException - * - * @return string - */ - public static function createSerializedData(ExtraFieldsCollection $extraFieldsCollection) - { - $extraData = ''; - - foreach ($extraFieldsCollection as $extraField) { - $data = $extraField->serialize(); - $extraData .= pack('vv', $extraField::getHeaderId(), \strlen($data)); - $extraData .= $data; - } - - $size = \strlen($extraData); - - if ($size < 0x0000 || $size > 0xffff) { - throw new ZipException('Size extra out of range: ' . $size . '. Extra data: ' . $extraData); - } - - return $extraData; - } - - /** - * A static factory method which creates a new Extra Field based on the - * given Header ID. - * The returned Extra Field still requires proper initialization, for - * example by calling ExtraField::readFrom. - * - * @param int $headerId an unsigned short integer (two bytes) which indicates - * the type of the returned Extra Field - * - * @throws ZipException if headerId is out of range - * - * @return ExtraField a new Extra Field or null if not support header id - */ - public static function create($headerId) - { - if ($headerId < 0x0000 || $headerId > 0xffff) { - throw new ZipException('headerId out of range'); - } - - if (isset(self::getRegistry()[$headerId])) { - $extraClassName = self::getRegistry()[$headerId]; - /** @var ExtraField $extraField */ - $extraField = new $extraClassName(); - - if ($headerId !== $extraField::getHeaderId()) { - throw new ZipException('Runtime error support headerId ' . $headerId); - } - } else { - $extraField = new DefaultExtraField($headerId); - } - - return $extraField; - } - - /** - * Registered extra field classes. - * - * @return array - */ - protected static function getRegistry() - { - if (self::$registry === null) { - self::$registry[WinZipAesEntryExtraField::getHeaderId()] = WinZipAesEntryExtraField::class; - self::$registry[NtfsExtraField::getHeaderId()] = NtfsExtraField::class; - self::$registry[Zip64ExtraField::getHeaderId()] = Zip64ExtraField::class; - self::$registry[ApkAlignmentExtraField::getHeaderId()] = ApkAlignmentExtraField::class; - self::$registry[JarMarkerExtraField::getHeaderId()] = JarMarkerExtraField::class; - } - - return self::$registry; - } - - /** - * @return WinZipAesEntryExtraField - */ - public static function createWinZipAesEntryExtra() - { - return new WinZipAesEntryExtraField(); - } - - /** - * @return NtfsExtraField - */ - public static function createNtfsExtra() - { - return new NtfsExtraField(); - } - - /** - * @param ZipEntry $entry - * - * @return Zip64ExtraField - */ - public static function createZip64Extra(ZipEntry $entry) - { - return new Zip64ExtraField($entry); - } - - /** - * @param ZipEntry $entry - * @param int $padding - * - * @return ApkAlignmentExtraField - */ - public static function createApkAlignExtra(ZipEntry $entry, $padding) - { - $padding = (int) $padding; - $multiple = 4; - - if (StringUtil::endsWith($entry->getName(), '.so')) { - $multiple = ApkAlignmentExtraField::ANDROID_COMMON_PAGE_ALIGNMENT_BYTES; - } - $extraField = new ApkAlignmentExtraField(); - $extraField->setMultiple($multiple); - $extraField->setPadding($padding); - - return $extraField; - } -} diff --git a/src/PhpZip/Extra/Fields/ApkAlignmentExtraField.php b/src/PhpZip/Extra/Fields/ApkAlignmentExtraField.php deleted file mode 100644 index 9548f63..0000000 --- a/src/PhpZip/Extra/Fields/ApkAlignmentExtraField.php +++ /dev/null @@ -1,115 +0,0 @@ -padding > 0) { - $args = array_merge( - ['vc*', $this->multiple], - array_fill(2, $this->padding, 0) - ); - - return \call_user_func_array('pack', $args); - } - - return pack('v', $this->multiple); - } - - /** - * Initializes this Extra Field by deserializing a Data Block. - * - * @param string $data - */ - public function deserialize($data) - { - $length = \strlen($data); - - if ($length < 2) { - // This is APK alignment field. - // FORMAT: - // * uint16 alignment multiple (in bytes) - // * remaining bytes -- padding to achieve alignment of data which starts after - // the extra field - throw new InvalidArgumentException( - 'Minimum 6 bytes of the extensible data block/field used for alignment of uncompressed entries.' - ); - } - $this->multiple = unpack('v', $data)[1]; - $this->padding = $length - 2; - } - - /** - * @return mixed - */ - public function getMultiple() - { - return $this->multiple; - } - - /** - * @return int - */ - public function getPadding() - { - return $this->padding; - } - - /** - * @param int $multiple - */ - public function setMultiple($multiple) - { - $this->multiple = $multiple; - } - - /** - * @param int $padding - */ - public function setPadding($padding) - { - $this->padding = $padding; - } -} diff --git a/src/PhpZip/Extra/Fields/DefaultExtraField.php b/src/PhpZip/Extra/Fields/DefaultExtraField.php deleted file mode 100644 index 231fa32..0000000 --- a/src/PhpZip/Extra/Fields/DefaultExtraField.php +++ /dev/null @@ -1,70 +0,0 @@ - 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/JarMarkerExtraField.php b/src/PhpZip/Extra/Fields/JarMarkerExtraField.php deleted file mode 100644 index 8914204..0000000 --- a/src/PhpZip/Extra/Fields/JarMarkerExtraField.php +++ /dev/null @@ -1,54 +0,0 @@ -mtime = PackUtil::unpackLongLE(substr($tagData, 0, 8)) / 10000000 - 11644473600; - $this->atime = PackUtil::unpackLongLE(substr($tagData, 8, 8)) / 10000000 - 11644473600; - $this->ctime = PackUtil::unpackLongLE(substr($tagData, 16, 8)) / 10000000 - 11644473600; - } - } - - /** - * Serializes a Data Block. - * - * @return string - */ - public function serialize() - { - $serialize = ''; - - if ($this->mtime !== null && $this->atime !== null && $this->ctime !== null) { - $mtimeLong = ($this->mtime + 11644473600) * 10000000; - $atimeLong = ($this->atime + 11644473600) * 10000000; - $ctimeLong = ($this->ctime + 11644473600) * 10000000; - - $serialize .= pack('Vvv', 0, 1, 8 * 3) - . PackUtil::packLongLE($mtimeLong) - . PackUtil::packLongLE($atimeLong) - . PackUtil::packLongLE($ctimeLong); - } - - return $serialize; - } - - /** - * @return int - */ - public function getMtime() - { - return $this->mtime; - } - - /** - * @param int $mtime - */ - public function setMtime($mtime) - { - $this->mtime = (int) $mtime; - } - - /** - * @return int - */ - public function getAtime() - { - return $this->atime; - } - - /** - * @param int $atime - */ - public function setAtime($atime) - { - $this->atime = (int) $atime; - } - - /** - * @return int - */ - public function getCtime() - { - return $this->ctime; - } - - /** - * @param int $ctime - */ - public function setCtime($ctime) - { - $this->ctime = (int) $ctime; - } -} diff --git a/src/PhpZip/Extra/Fields/WinZipAesEntryExtraField.php b/src/PhpZip/Extra/Fields/WinZipAesEntryExtraField.php deleted file mode 100644 index 582514a..0000000 --- a/src/PhpZip/Extra/Fields/WinZipAesEntryExtraField.php +++ /dev/null @@ -1,284 +0,0 @@ -do include the standard ZIP CRC-32 value. - * For use with @see WinZipAesEntryExtraField::setVendorVersion()}/@see - * WinZipAesEntryExtraField::getVendorVersion(). - */ - const VV_AE_1 = 1; - - /** - * Entries of this type do not include the standard ZIP CRC-32 value. - * For use with @see WinZipAesEntryExtraField::setVendorVersion()}/@see - * WinZipAesEntryExtraField::getVendorVersion(). - */ - const VV_AE_2 = 2; - - const KEY_STRENGTH_128BIT = 128; - - const KEY_STRENGTH_192BIT = 192; - - const KEY_STRENGTH_256BIT = 256; - - protected static $keyStrengths = [ - self::KEY_STRENGTH_128BIT => 0x01, - self::KEY_STRENGTH_192BIT => 0x02, - self::KEY_STRENGTH_256BIT => 0x03, - ]; - - protected static $encryptionMethods = [ - self::KEY_STRENGTH_128BIT => ZipFile::ENCRYPTION_METHOD_WINZIP_AES_128, - self::KEY_STRENGTH_192BIT => ZipFile::ENCRYPTION_METHOD_WINZIP_AES_192, - self::KEY_STRENGTH_256BIT => ZipFile::ENCRYPTION_METHOD_WINZIP_AES_256, - ]; - - /** - * Vendor version. - * - * @var int - */ - protected $vendorVersion = self::VV_AE_1; - - /** - * Encryption strength. - * - * @var int - */ - protected $encryptionStrength = self::KEY_STRENGTH_256BIT; - - /** - * Zip compression method. - * - * @var int - */ - protected $method; - - /** - * Returns the Header ID (type) of this Extra Field. - * The Header ID is an unsigned short integer (two bytes) - * which must be constant during the life cycle of this object. - * - * @return int - */ - public static function getHeaderId() - { - return 0x9901; - } - - /** - * Returns the vendor version. - * - * @see WinZipAesEntryExtraField::VV_AE_1 - * @see WinZipAesEntryExtraField::VV_AE_2 - */ - public function getVendorVersion() - { - return $this->vendorVersion & 0xffff; - } - - /** - * Sets the vendor version. - * - * @param int $vendorVersion the vendor version - * - * @throws ZipException unsupport vendor version - * - * @see WinZipAesEntryExtraField::VV_AE_1 - * @see WinZipAesEntryExtraField::VV_AE_2 - */ - public function setVendorVersion($vendorVersion) - { - if ($vendorVersion < self::VV_AE_1 || $vendorVersion > self::VV_AE_2) { - throw new ZipException($vendorVersion); - } - $this->vendorVersion = $vendorVersion; - } - - /** - * Returns vendor id. - * - * @return int - */ - public function getVendorId() - { - return self::VENDOR_ID; - } - - /** - * @throws ZipException - * - * @return bool|int - */ - public function getKeyStrength() - { - return self::keyStrength($this->encryptionStrength); - } - - /** - * @param int $encryptionStrength encryption strength as bits - * - * @throws ZipException if unsupport encryption strength - * - * @return int - */ - public static function keyStrength($encryptionStrength) - { - $flipKeyStrength = array_flip(self::$keyStrengths); - - if (!isset($flipKeyStrength[$encryptionStrength])) { - throw new ZipException('Unsupport encryption strength ' . $encryptionStrength); - } - - return $flipKeyStrength[$encryptionStrength]; - } - - /** - * Returns compression method. - * - * @return int - */ - public function getMethod() - { - return $this->method & 0xffff; - } - - /** - * Internal encryption method. - * - * @throws ZipException - * - * @return int - */ - public function getEncryptionMethod() - { - return isset(self::$encryptionMethods[$this->getKeyStrength()]) ? - self::$encryptionMethods[$this->getKeyStrength()] : - self::$encryptionMethods[self::KEY_STRENGTH_256BIT]; - } - - /** - * @param int $encryptionMethod - * - * @throws ZipException - * - * @return int - */ - 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. - * - * @param int $compressionMethod Compression method - * - * @throws ZipException compression method out of range - */ - public function setMethod($compressionMethod) - { - if ($compressionMethod < 0x0000 || $compressionMethod > 0xffff) { - throw new ZipException('Compression method out of range'); - } - $this->method = $compressionMethod; - } - - /** - * Set key strength. - * - * @param int $keyStrength - */ - public function setKeyStrength($keyStrength) - { - $this->encryptionStrength = self::encryptionStrength($keyStrength); - } - - /** - * Returns encryption strength. - * - * @param int $keyStrength key strength in bits - * - * @return int - */ - public static function encryptionStrength($keyStrength) - { - return isset(self::$keyStrengths[$keyStrength]) ? - self::$keyStrengths[$keyStrength] : - self::$keyStrengths[self::KEY_STRENGTH_128BIT]; - } - - /** - * Serializes a Data Block. - * - * @return string - */ - public function serialize() - { - return pack( - 'vvcv', - $this->vendorVersion, - self::VENDOR_ID, - $this->encryptionStrength, - $this->method - ); - } - - /** - * Initializes this Extra Field by deserializing a Data Block. - * - * @param string $data - * - * @throws ZipException - */ - public function deserialize($data) - { - $size = \strlen($data); - - if ($size !== self::DATA_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 ($unpack['vendorId'] !== self::VENDOR_ID) { - 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 deleted file mode 100644 index 7d424ac..0000000 --- a/src/PhpZip/Extra/Fields/Zip64ExtraField.php +++ /dev/null @@ -1,123 +0,0 @@ -setEntry($entry); - } - } - - /** - * @param ZipEntry $entry - */ - public function setEntry(ZipEntry $entry) - { - $this->entry = $entry; - } - - /** - * Returns the Header ID (type) of this Extra Field. - * The Header ID is an unsigned short integer (two bytes) - * which must be constant during the life cycle of this object. - * - * @return int - */ - public static function getHeaderId() - { - return 0x0001; - } - - /** - * Serializes a Data Block. - * - * @return string - */ - public function serialize() - { - if ($this->entry === null) { - throw new RuntimeException('entry is null'); - } - $data = ''; - // Write out Uncompressed Size. - $size = $this->entry->getSize(); - - if ($size >= 0xffffffff) { - $data .= PackUtil::packLongLE($size); - } - // Write out Compressed Size. - $compressedSize = $this->entry->getCompressedSize(); - - if ($compressedSize >= 0xffffffff) { - $data .= PackUtil::packLongLE($compressedSize); - } - // Write out Relative Header Offset. - $offset = $this->entry->getOffset(); - - if ($offset >= 0xffffffff) { - $data .= PackUtil::packLongLE($offset); - } - - return $data; - } - - /** - * Initializes this Extra Field by deserializing a Data Block. - * - * @param string $data - * - * @throws ZipException - */ - public function deserialize($data) - { - if ($this->entry === null) { - throw new RuntimeException('entry is null'); - } - $off = 0; - - // Read in Uncompressed Size. - if ($this->entry->getSize() === 0xffffffff) { - $this->entry->setSize(PackUtil::unpackLongLE(substr($data, $off, 8))); - $off += 8; - } - - // Read in Compressed Size. - if ($this->entry->getCompressedSize() === 0xffffffff) { - $this->entry->setCompressedSize(PackUtil::unpackLongLE(substr($data, $off, 8))); - $off += 8; - } - - // Read in Relative Header Offset. - if ($this->entry->getOffset() === 0xffffffff) { - $this->entry->setOffset(PackUtil::unpackLongLE(substr($data, $off, 8))); - } - } -} diff --git a/src/PhpZip/Model/EndOfCentralDirectory.php b/src/PhpZip/Model/EndOfCentralDirectory.php deleted file mode 100644 index b228fb4..0000000 --- a/src/PhpZip/Model/EndOfCentralDirectory.php +++ /dev/null @@ -1,157 +0,0 @@ -entryCount = $entryCount; - $this->cdOffset = $cdOffset; - $this->cdSize = $cdSize; - $this->zip64 = $zip64; - $this->comment = $comment; - } - - /** - * @param string|null $comment - */ - public function setComment($comment) - { - $this->comment = $comment; - } - - /** - * @return int - */ - public function getEntryCount() - { - return $this->entryCount; - } - - /** - * @return int - */ - public function getCdOffset() - { - return $this->cdOffset; - } - - /** - * @return int - */ - public function getCdSize() - { - return $this->cdSize; - } - - /** - * @return string|null - */ - public function getComment() - { - return $this->comment; - } - - /** - * @return bool - */ - public function isZip64() - { - return $this->zip64; - } -} diff --git a/src/PhpZip/Model/Entry/OutputOffsetEntry.php b/src/PhpZip/Model/Entry/OutputOffsetEntry.php deleted file mode 100644 index b968212..0000000 --- a/src/PhpZip/Model/Entry/OutputOffsetEntry.php +++ /dev/null @@ -1,48 +0,0 @@ -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 deleted file mode 100644 index f376eb8..0000000 --- a/src/PhpZip/Model/Entry/ZipAbstractEntry.php +++ /dev/null @@ -1,912 +0,0 @@ -extraFieldsCollection = new ExtraFieldsCollection(); - } - - /** - * @param ZipEntry $entry - * - * @throws ZipException - */ - public function setEntry(ZipEntry $entry) - { - $this->setName($entry->getName()); - $this->setSoftwareVersion($entry->getSoftwareVersion()); - $this->setCreatedOS($entry->getCreatedOS()); - $this->setExtractedOS($entry->getExtractedOS()); - $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->setInternalAttributes($entry->getInternalAttributes()); - $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()); - } - - /** - * Returns the ZIP entry name. - * - * @return string - */ - public function getName() - { - return $this->name; - } - - /** - * Set entry name. - * - * @param string $name New entry name - * - * @throws ZipException - * - * @return ZipEntry - */ - public function setName($name) - { - $length = \strlen($name); - - if ($length < 0x0000 || $length > 0xffff) { - throw new ZipException('Illegal zip entry name parameter'); - } - $this->setGeneralPurposeBitFlag(self::GPBF_UTF8, true); - $this->name = $name; - $this->externalAttributes = $this->isDirectory() ? 0x10 : 0; - - 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->generalPurposeBitFlags |= $mask; - } else { - $this->generalPurposeBitFlags &= ~$mask; - } - - return $this; - } - - /** - * @return int Get platform - * - * @deprecated Use {@see ZipEntry::getCreatedOS()} - * @noinspection PhpUsageOfSilenceOperatorInspection - */ - public function getPlatform() - { - @trigger_error('ZipEntry::getPlatform() is deprecated. Use ZipEntry::getCreatedOS()', \E_USER_DEPRECATED); - - return $this->getCreatedOS(); - } - - /** - * @param int $platform - * - * @throws ZipException - * - * @return ZipEntry - * - * @deprecated Use {@see ZipEntry::setCreatedOS()} - * @noinspection PhpUsageOfSilenceOperatorInspection - */ - public function setPlatform($platform) - { - @trigger_error('ZipEntry::setPlatform() is deprecated. Use ZipEntry::setCreatedOS()', \E_USER_DEPRECATED); - - return $this->setCreatedOS($platform); - } - - /** - * @return int platform - */ - public function getCreatedOS() - { - return $this->createdOS; - } - - /** - * Set platform. - * - * @param int $platform - * - * @throws ZipException - * - * @return ZipEntry - */ - public function setCreatedOS($platform) - { - $platform = (int) $platform; - - if ($platform < 0x00 || $platform > 0xff) { - throw new ZipException('Platform out of range'); - } - $this->createdOS = $platform; - - return $this; - } - - /** - * @return int - */ - public function getExtractedOS() - { - return $this->extractedOS; - } - - /** - * Set extracted OS. - * - * @param int $platform - * - * @throws ZipException - * - * @return ZipEntry - */ - public function setExtractedOS($platform) - { - $platform = (int) $platform; - - if ($platform < 0x00 || $platform > 0xff) { - throw new ZipException('Platform out of range'); - } - $this->extractedOS = $platform; - - return $this; - } - - /** - * @return int - */ - public function getSoftwareVersion() - { - return $this->softwareVersion; - } - - /** - * @param int $softwareVersion - * - * @return ZipEntry - */ - public function setSoftwareVersion($softwareVersion) - { - $this->softwareVersion = (int) $softwareVersion; - - return $this; - } - - /** - * Version needed to extract. - * - * @return int - */ - public function getVersionNeededToExtract() - { - if ($this->versionNeededToExtract === self::UNKNOWN) { - $method = $this->getMethod(); - - if ($method === self::METHOD_WINZIP_AES) { - return 51; - } - - if ($method === ZipFile::METHOD_BZIP2) { - return 46; - } - - if ($this->isZip64ExtensionsRequired()) { - return 45; - } - - return $method === ZipFile::METHOD_DEFLATED || $this->isDirectory() ? 20 : 10; - } - - return $this->versionNeededToExtract; - } - - /** - * Set version needed to extract. - * - * @param int $version - * - * @return ZipEntry - */ - public function setVersionNeededToExtract($version) - { - $this->versionNeededToExtract = $version; - - return $this; - } - - /** - * @return bool - */ - public function isZip64ExtensionsRequired() - { - return $this->getCompressedSize() >= 0xffffffff - || $this->getSize() >= 0xffffffff; - } - - /** - * Returns the compressed size of this entry. - * - * @see int - */ - public function getCompressedSize() - { - return $this->compressedSize; - } - - /** - * Sets the compressed size of this entry. - * - * @param int $compressedSize the Compressed Size - * - * @return ZipEntry - */ - public function setCompressedSize($compressedSize) - { - $this->compressedSize = $compressedSize; - - return $this; - } - - /** - * Returns the uncompressed size of this entry. - * - * @see ZipEntry::setCompressedSize - */ - public function getSize() - { - return $this->size; - } - - /** - * Sets the uncompressed size of this entry. - * - * @param int $size the (Uncompressed) Size - * - * @return ZipEntry - */ - public function setSize($size) - { - $this->size = $size; - - return $this; - } - - /** - * Return relative Offset Of Local File Header. - * - * @return int - */ - public function getOffset() - { - return $this->offset; - } - - /** - * @param int $offset - * - * @return ZipEntry - */ - public function setOffset($offset) - { - $this->offset = (int) $offset; - - return $this; - } - - /** - * Returns the General Purpose Bit Flags. - * - * @return int - */ - public function getGeneralPurposeBitFlags() - { - return $this->generalPurposeBitFlags & 0xffff; - } - - /** - * Sets the General Purpose Bit Flags. - * - * @param mixed $general - * - * @throws ZipException - * - * @return ZipEntry - * - * @var int general - */ - public function setGeneralPurposeBitFlags($general) - { - if ($general < 0x0000 || $general > 0xffff) { - throw new ZipException('general out of range'); - } - $this->generalPurposeBitFlags = $general; - - if ($this->method === ZipFile::METHOD_DEFLATED) { - $bit1 = $this->getGeneralPurposeBitFlag(self::GPBF_COMPRESSION_FLAG1); - $bit2 = $this->getGeneralPurposeBitFlag(self::GPBF_COMPRESSION_FLAG2); - - if ($bit1 && !$bit2) { - $this->compressionLevel = ZipFile::LEVEL_BEST_COMPRESSION; - } elseif (!$bit1 && $bit2) { - $this->compressionLevel = ZipFile::LEVEL_FAST; - } elseif ($bit1 && $bit2) { - $this->compressionLevel = ZipFile::LEVEL_SUPER_FAST; - } else { - $this->compressionLevel = ZipFile::LEVEL_DEFAULT_COMPRESSION; - } - } - - return $this; - } - - /** - * Returns true if and only if this ZIP entry is encrypted. - * - * @return bool - */ - public function isEncrypted() - { - return $this->getGeneralPurposeBitFlag(self::GPBF_ENCRYPTED); - } - - /** - * Returns the indexed General Purpose Bit Flag. - * - * @param int $mask - * - * @return bool - */ - public function getGeneralPurposeBitFlag($mask) - { - return ($this->generalPurposeBitFlags & $mask) !== 0; - } - - /** - * Sets the encryption property to false and removes any other - * encryption artifacts. - * - * @throws ZipException - * - * @return ZipEntry - */ - public function disableEncryption() - { - $this->setEncrypted(false); - $headerId = WinZipAesEntryExtraField::getHeaderId(); - - if (isset($this->extraFieldsCollection[$headerId])) { - /** @var WinZipAesEntryExtraField $field */ - $field = $this->extraFieldsCollection[$headerId]; - - if ($this->getMethod() === self::METHOD_WINZIP_AES) { - $this->setMethod($field === null ? self::UNKNOWN : $field->getMethod()); - } - unset($this->extraFieldsCollection[$headerId]); - } - $this->password = null; - - return $this; - } - - /** - * Sets the encryption flag for this ZIP entry. - * - * @param bool $encrypted - * - * @return ZipEntry - */ - public function setEncrypted($encrypted) - { - $encrypted = (bool) $encrypted; - $this->setGeneralPurposeBitFlag(self::GPBF_ENCRYPTED, $encrypted); - - return $this; - } - - /** - * Returns the compression method for this entry. - * - * @return int - */ - public function getMethod() - { - return $this->method; - } - - /** - * Sets the compression method for this entry. - * - * @param int $method - * - * @throws ZipException if method is not STORED, DEFLATED, BZIP2 or UNKNOWN - * - * @return ZipEntry - */ - public function setMethod($method) - { - if ($method === self::UNKNOWN) { - $this->method = $method; - - return $this; - } - - if ($method < 0x0000 || $method > 0xffff) { - throw new ZipException('method out of range: ' . $method); - } - switch ($method) { - case self::METHOD_WINZIP_AES: - case ZipFile::METHOD_STORED: - case ZipFile::METHOD_DEFLATED: - case ZipFile::METHOD_BZIP2: - $this->method = $method; - break; - - default: - throw new ZipException($this->name . " (unsupported compression method {$method})"); - } - - return $this; - } - - /** - * Get Unix Timestamp. - * - * @return int - */ - public function getTime() - { - if ($this->getDosTime() === self::UNKNOWN) { - return self::UNKNOWN; - } - - return DateTimeConverter::toUnixTimestamp($this->getDosTime()); - } - - /** - * Get Dos Time. - * - * @return int - */ - public function getDosTime() - { - return $this->dosTime; - } - - /** - * Set Dos Time. - * - * @param int $dosTime - * - * @throws ZipException - * - * @return ZipEntry - */ - public function setDosTime($dosTime) - { - $dosTime = (int) $dosTime; - - if ($dosTime < 0x00000000 || $dosTime > 0xffffffff) { - throw new ZipException('DosTime out of range'); - } - $this->dosTime = $dosTime; - - return $this; - } - - /** - * Set time from unix timestamp. - * - * @param int $unixTimestamp - * - * @throws ZipException - * - * @return ZipEntry - */ - public function setTime($unixTimestamp) - { - $known = $unixTimestamp !== self::UNKNOWN; - - if ($known) { - $this->dosTime = DateTimeConverter::toDosTime($unixTimestamp); - } else { - $this->dosTime = 0; - } - - return $this; - } - - /** - * Returns the external file attributes. - * - * @return int the external file attributes - */ - public function getExternalAttributes() - { - return $this->externalAttributes; - } - - /** - * Sets the external file attributes. - * - * @param int $externalAttributes the external file attributes - * - * @return ZipEntry - */ - public function setExternalAttributes($externalAttributes) - { - $this->externalAttributes = $externalAttributes; - - return $this; - } - - /** - * Sets the internal file attributes. - * - * @param int $attributes the internal file attributes - * - * @return ZipEntry - */ - public function setInternalAttributes($attributes) - { - $this->internalAttributes = (int) $attributes; - - return $this; - } - - /** - * Returns the internal file attributes. - * - * @return int the internal file attributes - */ - public function getInternalAttributes() - { - return $this->internalAttributes; - } - - /** - * Returns true if and only if this ZIP entry represents a directory entry - * (i.e. end with '/'). - * - * @return bool - */ - public function isDirectory() - { - return StringUtil::endsWith($this->name, '/'); - } - - /** - * @return ExtraFieldsCollection - */ - public function &getExtraFieldsCollection() - { - return $this->extraFieldsCollection; - } - - /** - * Returns a protective copy of the serialized Extra Fields. - * - * @throws ZipException - * - * @return string - */ - public function getExtra() - { - return ExtraFieldsFactory::createSerializedData($this->extraFieldsCollection); - } - - /** - * Sets the serialized Extra Fields by making a protective copy. - * Note that this method parses the serialized Extra Fields according to - * the ZIP File Format Specification and limits its size to 64 KB. - * Therefore, this property cannot not be used to hold arbitrary - * (application) data. - * Consider storing such data in a separate entry instead. - * - * @param string $data the byte array holding the serialized Extra Fields - * - * @throws ZipException if the serialized Extra Fields exceed 64 KB - * - * @return ZipEntry - */ - public function setExtra($data) - { - $this->extraFieldsCollection = ExtraFieldsFactory::createExtraFieldCollections($data, $this); - - return $this; - } - - /** - * Returns comment entry. - * - * @return string - */ - public function getComment() - { - return $this->comment !== null ? $this->comment : ''; - } - - /** - * Set entry comment. - * - * @param string|null $comment - * - * @throws ZipException - * - * @return ZipEntry - */ - public function setComment($comment) - { - if ($comment !== null) { - $commentLength = \strlen($comment); - - if ($commentLength < 0x0000 || $commentLength > 0xffff) { - throw new ZipException('Comment too long'); - } - $this->setGeneralPurposeBitFlag(self::GPBF_UTF8, true); - } - $this->comment = $comment; - - return $this; - } - - /** - * @return bool - */ - public function isDataDescriptorRequired() - { - return ($this->getCrc() | $this->getCompressedSize() | $this->getSize()) === self::UNKNOWN; - } - - /** - * Return crc32 content or 0 for WinZip AES v2. - * - * @return int - */ - public function getCrc() - { - return $this->crc; - } - - /** - * Set crc32 content. - * - * @param int $crc - * - * @return ZipEntry - */ - public function setCrc($crc) - { - $this->crc = (int) $crc; - - return $this; - } - - /** - * @return string - */ - public function getPassword() - { - return $this->password; - } - - /** - * Set password and encryption method from entry. - * - * @param string $password - * @param int|null $encryptionMethod - * - * @throws ZipException - * - * @return ZipEntry - */ - public function setPassword($password, $encryptionMethod = null) - { - $this->password = $password; - - if ($encryptionMethod !== null) { - $this->setEncryptionMethod($encryptionMethod); - } - - if (!empty($this->password)) { - $this->setEncrypted(true); - } else { - $this->disableEncryption(); - } - - return $this; - } - - /** - * @return int - */ - public function getEncryptionMethod() - { - return $this->encryptionMethod; - } - - /** - * Set encryption method. - * - * @param int $encryptionMethod - * - * @throws ZipException - * - * @return ZipEntry - * - * @see ZipFile::ENCRYPTION_METHOD_WINZIP_AES_256 - * @see ZipFile::ENCRYPTION_METHOD_TRADITIONAL - * @see ZipFile::ENCRYPTION_METHOD_WINZIP_AES_128 - * @see ZipFile::ENCRYPTION_METHOD_WINZIP_AES_192 - */ - public function setEncryptionMethod($encryptionMethod) - { - if ($encryptionMethod !== null) { - if ( - $encryptionMethod !== ZipFile::ENCRYPTION_METHOD_TRADITIONAL - && $encryptionMethod !== ZipFile::ENCRYPTION_METHOD_WINZIP_AES_128 - && $encryptionMethod !== ZipFile::ENCRYPTION_METHOD_WINZIP_AES_192 - && $encryptionMethod !== ZipFile::ENCRYPTION_METHOD_WINZIP_AES_256 - ) { - throw new ZipException('Invalid encryption method'); - } - $this->encryptionMethod = $encryptionMethod; - } - - return $this; - } - - /** - * @return int - */ - public function getCompressionLevel() - { - return $this->compressionLevel; - } - - /** - * @param int $compressionLevel - * - * @return ZipEntry - */ - 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 $this; - } - - /** - * Clone extra fields. - */ - public function __clone() - { - $this->extraFieldsCollection = clone $this->extraFieldsCollection; - } -} diff --git a/src/PhpZip/Model/Entry/ZipChangesEntry.php b/src/PhpZip/Model/Entry/ZipChangesEntry.php deleted file mode 100644 index c17415f..0000000 --- a/src/PhpZip/Model/Entry/ZipChangesEntry.php +++ /dev/null @@ -1,69 +0,0 @@ -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. - * - * @throws ZipException - * - * @return string|null - */ - public function getEntryContent() - { - return $this->entry->getEntryContent(); - } - - /** - * @return ZipSourceEntry - */ - public function getSourceEntry() - { - return $this->entry; - } -} diff --git a/src/PhpZip/Model/Entry/ZipNewEntry.php b/src/PhpZip/Model/Entry/ZipNewEntry.php deleted file mode 100644 index 5930a74..0000000 --- a/src/PhpZip/Model/Entry/ZipNewEntry.php +++ /dev/null @@ -1,68 +0,0 @@ -content = $content; - } - - /** - * Returns an string content of the given entry. - * - * @return string|null - */ - public function getEntryContent() - { - if (\is_resource($this->content)) { - if (stream_get_meta_data($this->content)['seekable']) { - rewind($this->content); - } - - return stream_get_contents($this->content); - } - - return $this->content; - } - - /** - * Clone extra fields. - */ - public function __clone() - { - $this->clone = true; - parent::__clone(); - } - - public function __destruct() - { - if (!$this->clone && $this->content !== null && \is_resource($this->content)) { - fclose($this->content); - $this->content = null; - } - } -} diff --git a/src/PhpZip/Model/Entry/ZipNewFileEntry.php b/src/PhpZip/Model/Entry/ZipNewFileEntry.php deleted file mode 100644 index 7a2bead..0000000 --- a/src/PhpZip/Model/Entry/ZipNewFileEntry.php +++ /dev/null @@ -1,57 +0,0 @@ -file = $file; - } - - /** - * Returns an string content of the given entry. - * - * @return string|null - */ - public function getEntryContent() - { - if (!is_file($this->file)) { - throw new RuntimeException("File {$this->file} does not exist."); - } - - return file_get_contents($this->file); - } -} diff --git a/src/PhpZip/Model/Entry/ZipSourceEntry.php b/src/PhpZip/Model/Entry/ZipSourceEntry.php deleted file mode 100644 index 5a992ea..0000000 --- a/src/PhpZip/Model/Entry/ZipSourceEntry.php +++ /dev/null @@ -1,96 +0,0 @@ -inputStream = $inputStream; - } - - /** - * @return ZipInputStreamInterface - */ - public function getInputStream() - { - return $this->inputStream; - } - - /** - * Returns an string content of the given entry. - * - * @throws ZipException - * - * @return string - */ - public function getEntryContent() - { - if ($this->entryContent === null) { - // In order not to unpack again, we cache the content in memory or on disk - $content = $this->inputStream->readEntryContent($this); - - if ($this->getSize() < self::MAX_SIZE_CACHED_CONTENT_IN_MEMORY) { - $this->entryContent = $content; - } else { - $this->entryContent = fopen('php://temp', 'r+b'); - fwrite($this->entryContent, $content); - } - - return $content; - } - - if (\is_resource($this->entryContent)) { - rewind($this->entryContent); - - return stream_get_contents($this->entryContent); - } - - return $this->entryContent; - } - - /** - * Clone extra fields. - */ - public function __clone() - { - $this->clone = true; - parent::__clone(); - } - - public function __destruct() - { - if (!$this->clone && $this->entryContent !== null && \is_resource($this->entryContent)) { - fclose($this->entryContent); - } - } -} diff --git a/src/PhpZip/Model/ZipEntry.php b/src/PhpZip/Model/ZipEntry.php deleted file mode 100644 index 1360a71..0000000 --- a/src/PhpZip/Model/ZipEntry.php +++ /dev/null @@ -1,536 +0,0 @@ - 'FAT', - self::MADE_BY_AMIGA => 'Amiga', - self::MADE_BY_OPEN_VMS => 'OpenVMS', - self::MADE_BY_UNIX => 'UNIX', - self::MADE_BY_VM_CMS => 'VM/CMS', - self::MADE_BY_ATARI => 'Atari ST', - self::MADE_BY_OS_2 => 'OS/2 H.P.F.S.', - self::MADE_BY_MACINTOSH => 'Macintosh', - self::MADE_BY_Z_SYSTEM => 'Z-System', - self::MADE_BY_CP_M => 'CP/M', - self::MADE_BY_WINDOWS_NTFS => 'Windows NTFS', - self::MADE_BY_MVS => 'MVS (OS/390 - Z/OS)', - self::MADE_BY_VSE => 'VSE', - self::MADE_BY_ACORN_RISC => 'Acorn Risc', - self::MADE_BY_VFAT => 'VFAT', - self::MADE_BY_ALTERNATE_MVS => 'Alternate MVS', - self::MADE_BY_BEOS => 'BeOS', - self::MADE_BY_TANDEM => 'Tandem', - self::MADE_BY_OS_400 => 'OS/400', - self::MADE_BY_OS_X => 'Mac OS X', - ]; - - private static $compressionMethodNames = [ - ZipEntry::UNKNOWN => 'unknown', - ZipFile::METHOD_STORED => 'no compression', - 1 => 'shrink', - 2 => 'reduce level 1', - 3 => 'reduce level 2', - 4 => 'reduce level 3', - 5 => 'reduce level 4', - 6 => 'implode', - 7 => 'reserved for Tokenizing compression algorithm', - ZipFile::METHOD_DEFLATED => 'deflate', - 9 => 'deflate64', - 10 => 'PKWARE Data Compression Library Imploding (old IBM TERSE)', - 11 => 'reserved by PKWARE', - 12 => 'bzip2', - 13 => 'reserved by PKWARE', - 14 => 'LZMA (EFS)', - 15 => 'reserved by PKWARE', - 16 => 'reserved by PKWARE', - 17 => 'reserved by PKWARE', - 18 => 'IBM TERSE', - 19 => 'IBM LZ77 z Architecture (PFS)', - 97 => 'WavPack', - 98 => 'PPMd version I, Rev 1', - ZipEntry::METHOD_WINZIP_AES => 'WinZip AES', - ]; - - /** @var string */ - 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 $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. - * - * @param ZipEntry $entry - * - * @throws ZipException - * @noinspection PhpMissingBreakStatementInspection - */ - public function __construct(ZipEntry $entry) - { - $mtime = $entry->getTime(); - $atime = null; - $ctime = null; - - $field = $entry->getExtraFieldsCollection()->get(NtfsExtraField::getHeaderId()); - - if ($field instanceof NtfsExtraField) { - /** - * @var NtfsExtraField $field - */ - $atime = $field->getAtime(); - $ctime = $field->getCtime(); - $mtime = $field->getMtime(); - } - - $this->name = $entry->getName(); - $this->folder = $entry->isDirectory(); - $this->size = $entry->getSize(); - $this->compressedSize = $entry->getCompressedSize(); - $this->mtime = $mtime; - $this->ctime = $ctime; - $this->atime = $atime; - $this->encrypted = $entry->isEncrypted(); - $this->encryptionMethod = $entry->getEncryptionMethod(); - $this->comment = $entry->getComment(); - $this->crc = $entry->getCrc(); - $this->compressionMethod = self::getMethodId($entry); - $this->methodName = self::getEntryMethodName($entry); - $this->platform = self::getPlatformName($entry); - $this->version = $entry->getVersionNeededToExtract(); - $this->compressionLevel = $entry->getCompressionLevel(); - - $attributes = str_repeat(' ', 12); - $externalAttributes = $entry->getExternalAttributes(); - $xattr = (($externalAttributes >> 16) & 0xFFFF); - switch ($entry->getCreatedOS()) { - case self::MADE_BY_MS_DOS: - case self::MADE_BY_WINDOWS_NTFS: - if ($entry->getCreatedOS() !== self::MADE_BY_MS_DOS || - ($xattr & self::UNX_IRWXU) !== - (self::UNX_IRUSR | - (!($externalAttributes & 1) << 7) | - (($externalAttributes & 0x10) << 2)) - ) { - $xattr = $externalAttributes & 0xFF; - $attributes = '.r.-... '; - $attributes[2] = ($xattr & 0x01) ? '-' : 'w'; - $attributes[5] = ($xattr & 0x02) ? 'h' : '-'; - $attributes[6] = ($xattr & 0x04) ? 's' : '-'; - $attributes[4] = ($xattr & 0x20) ? 'a' : '-'; - - if ($xattr & 0x10) { - $attributes[0] = 'd'; - $attributes[3] = 'x'; - } else { - $attributes[0] = '-'; - } - - if ($xattr & 0x08) { - $attributes[0] = 'V'; - } else { - $ext = strtolower(pathinfo($entry->getName(), \PATHINFO_EXTENSION)); - - if (\in_array($ext, ['com', 'exe', 'btm', 'cmd', 'bat'])) { - $attributes[3] = 'x'; - } - } - break; - } // else: fall through! - - // no break - default: // assume Unix-like - switch ($xattr & self::UNX_IFMT) { - case self::UNX_IFDIR: - $attributes[0] = 'd'; - break; - - case self::UNX_IFREG: - $attributes[0] = '-'; - break; - - case self::UNX_IFLNK: - $attributes[0] = 'l'; - break; - - case self::UNX_IFBLK: - $attributes[0] = 'b'; - break; - - case self::UNX_IFCHR: - $attributes[0] = 'c'; - break; - - case self::UNX_IFIFO: - $attributes[0] = 'p'; - break; - - case self::UNX_IFSOCK: - $attributes[0] = 's'; - break; - default: - $attributes[0] = '?'; - break; - } - $attributes[1] = ($xattr & self::UNX_IRUSR) ? 'r' : '-'; - $attributes[4] = ($xattr & self::UNX_IRGRP) ? 'r' : '-'; - $attributes[7] = ($xattr & self::UNX_IROTH) ? 'r' : '-'; - $attributes[2] = ($xattr & self::UNX_IWUSR) ? 'w' : '-'; - $attributes[5] = ($xattr & self::UNX_IWGRP) ? 'w' : '-'; - $attributes[8] = ($xattr & self::UNX_IWOTH) ? 'w' : '-'; - - 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 - } - $this->attributes = trim($attributes); - } - - /** - * @param ZipEntry $entry - * - * @throws ZipException - * - * @return int - */ - private static function getMethodId(ZipEntry $entry) - { - $method = $entry->getMethod(); - - if ($entry->isEncrypted() && $entry->getMethod() === ZipEntry::METHOD_WINZIP_AES) { - $field = $entry->getExtraFieldsCollection()->get(WinZipAesEntryExtraField::getHeaderId()); - - if ($field !== null) { - /** @var WinZipAesEntryExtraField $field */ - $method = $field->getMethod(); - } - } - - return $method; - } - - /** - * @param ZipEntry $entry - * - * @throws ZipException - * - * @return string - */ - private static function getEntryMethodName(ZipEntry $entry) - { - $return = ''; - - $compressionMethod = $entry->getMethod(); - - if ($entry->isEncrypted()) { - if ($entry->getMethod() === ZipEntry::METHOD_WINZIP_AES) { - $return .= ucfirst(self::$compressionMethodNames[$entry->getMethod()]); - /** @var WinZipAesEntryExtraField|null $field */ - $field = $entry->getExtraFieldsCollection()->get(WinZipAesEntryExtraField::getHeaderId()); - - if ($field !== null) { - $return .= '-' . $field->getKeyStrength(); - $compressionMethod = $field->getMethod(); - } - } else { - $return .= 'ZipCrypto'; - } - - $return .= ' '; - } - - if (isset(self::$compressionMethodNames[$compressionMethod])) { - $return .= ucfirst(self::$compressionMethodNames[$compressionMethod]); - } else { - $return .= 'unknown'; - } - - return $return; - } - - /** - * @param ZipEntry $entry - * - * @return string - */ - public static function getPlatformName(ZipEntry $entry) - { - if (isset(self::$platformNames[$entry->getCreatedOS()])) { - return self::$platformNames[$entry->getCreatedOS()]; - } - - return 'unknown'; - } - - /** - * @return string - */ - public function getName() - { - return $this->name; - } - - /** - * @return string - * - * @deprecated use \PhpZip\Model\ZipInfo::getName() - */ - public function getPath() - { - return $this->getName(); - } - - /** - * @return bool - */ - public function isFolder() - { - return $this->folder; - } - - /** - * @return int - */ - public function getSize() - { - return $this->size; - } - - /** - * @return int - */ - public function getCompressedSize() - { - return $this->compressedSize; - } - - /** - * @return int - */ - public function getMtime() - { - return $this->mtime; - } - - /** - * @return int|null - */ - public function getCtime() - { - return $this->ctime; - } - - /** - * @return int|null - */ - public function getAtime() - { - return $this->atime; - } - - /** - * @return string - */ - public function getAttributes() - { - return $this->attributes; - } - - /** - * @return bool - */ - public function isEncrypted() - { - return $this->encrypted; - } - - /** - * @return string|null - */ - public function getComment() - { - return $this->comment; - } - - /** - * @return int - */ - public function getCrc() - { - return $this->crc; - } - - /** - * @return string - * - * @deprecated use \PhpZip\Model\ZipInfo::getMethodName() - */ - public function getMethod() - { - return $this->getMethodName(); - } - - /** - * @return string - */ - public function getMethodName() - { - return $this->methodName; - } - - /** - * @return string - */ - public function getPlatform() - { - return $this->platform; - } - - /** - * @return int - */ - public function getVersion() - { - return $this->version; - } - - /** - * @return int|null - */ - public function getEncryptionMethod() - { - 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 - */ - public function __toString() - { - return __CLASS__ . ' {' - . 'Name="' . $this->getName() . '", ' - . ($this->isFolder() ? 'Folder, ' : '') - . 'Size="' . FilesUtil::humanSize($this->getSize()) . '"' - . ', Compressed size="' . FilesUtil::humanSize($this->getCompressedSize()) . '"' - . ', Modified time="' . date(\DATE_W3C, $this->getMtime()) . '", ' - . ($this->getCtime() !== null ? 'Created time="' . date(\DATE_W3C, $this->getCtime()) . '", ' : '') - . ($this->getAtime() !== null ? 'Accessed time="' . date(\DATE_W3C, $this->getAtime()) . '", ' : '') - . ($this->isEncrypted() ? 'Encrypted, ' : '') - . (!empty($this->comment) ? 'Comment="' . $this->getComment() . '", ' : '') - . (!empty($this->crc) ? 'Crc=0x' . dechex($this->getCrc()) . ', ' : '') - . 'Method name="' . $this->getMethodName() . '", ' - . 'Attributes="' . $this->getAttributes() . '", ' - . 'Platform="' . $this->getPlatform() . '", ' - . 'Version=' . $this->getVersion() - . '}'; - } -} diff --git a/src/PhpZip/Model/ZipModel.php b/src/PhpZip/Model/ZipModel.php deleted file mode 100644 index 7e5b0af..0000000 --- a/src/PhpZip/Model/ZipModel.php +++ /dev/null @@ -1,367 +0,0 @@ -inputEntries = $entries; - $model->outEntries = $entries; - $model->archiveComment = $endOfCentralDirectory->getComment(); - $model->zip64 = $endOfCentralDirectory->isZip64(); - - return $model; - } - - /** - * @return string|null - */ - public function getArchiveComment() - { - if ($this->archiveCommentChanged) { - return $this->archiveCommentChanges; - } - - return $this->archiveComment; - } - - /** - * @param string $comment - */ - public function setArchiveComment($comment) - { - if ($comment !== null && $comment !== '') { - $comment = (string) $comment; - $length = \strlen($comment); - - if ($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 string|null $password - * - * @throws ZipException - */ - public function setReadPassword($password) - { - foreach ($this->inputEntries as $entry) { - if ($entry->isEncrypted()) { - $entry->setPassword($password); - } - } - } - - /** - * @param string $entryName - * @param string $password - * - * @throws ZipEntryNotFoundException - * @throws ZipException - */ - public function setReadPasswordEntry($entryName, $password) - { - if (!isset($this->inputEntries[$entryName])) { - throw new ZipEntryNotFoundException($entryName); - } - - if ($this->inputEntries[$entryName]->isEncrypted()) { - $this->inputEntries[$entryName]->setPassword($password); - } - } - - /** - * @return int|null - */ - public function getZipAlign() - { - return $this->zipAlign; - } - - /** - * @param int|null $zipAlign - */ - public function setZipAlign($zipAlign) - { - $this->zipAlign = $zipAlign === null ? null : (int) $zipAlign; - } - - /** - * @return bool - */ - public function isZipAlign() - { - return $this->zipAlign !== null; - } - - /** - * @param string|null $writePassword - */ - public function setWritePassword($writePassword) - { - $this->matcher()->all()->setPassword($writePassword); - } - - /** - * Remove password. - */ - public function removePassword() - { - $this->matcher()->all()->setPassword(null); - } - - /** - * @param string|ZipEntry $entryName - */ - public function removePasswordEntry($entryName) - { - $this->matcher()->add($entryName)->setPassword(null); - } - - /** - * @return bool - */ - public function isArchiveCommentChanged() - { - return $this->archiveCommentChanged; - } - - /** - * @param string|ZipEntry $old - * @param string|ZipEntry $new - * - * @throws ZipException - */ - public function renameEntry($old, $new) - { - $old = $old instanceof ZipEntry ? $old->getName() : (string) $old; - $new = $new instanceof ZipEntry ? $new->getName() : (string) $new; - - if (isset($this->outEntries[$new])) { - throw new InvalidArgumentException('New entry name ' . $new . ' is exists.'); - } - - $entry = $this->getEntryForChanges($old); - $entry->setName($new); - $this->deleteEntry($old); - $this->addEntry($entry); - } - - /** - * @param string|ZipEntry $entry - * - * @throws ZipEntryNotFoundException - * @throws ZipException - * - * @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 - * - * @throws ZipEntryNotFoundException - * - * @return ZipEntry - */ - public function getEntry($entryName) - { - $entryName = $entryName instanceof ZipEntry ? $entryName->getName() : (string) $entryName; - - if (isset($this->outEntries[$entryName])) { - return $this->outEntries[$entryName]; - } - - throw new ZipEntryNotFoundException($entryName); - } - - /** - * @param string|ZipEntry $entry - * - * @return bool - */ - public function deleteEntry($entry) - { - $entry = $entry instanceof ZipEntry ? $entry->getName() : (string) $entry; - - if (isset($this->outEntries[$entry])) { - unset($this->outEntries[$entry]); - - return true; - } - - return false; - } - - /** - * @param ZipEntry $entry - */ - public function addEntry(ZipEntry $entry) - { - $this->outEntries[$entry->getName()] = $entry; - } - - /** - * Get all entries with changes. - * - * @return ZipEntry[] - */ - public function &getEntries() - { - return $this->outEntries; - } - - /** - * @param string|ZipEntry $entryName - * - * @return bool - */ - public function hasEntry($entryName) - { - $entryName = $entryName instanceof ZipEntry ? $entryName->getName() : (string) $entryName; - - return isset($this->outEntries[$entryName]); - } - - /** - * Delete all entries. - */ - public function deleteAll() - { - $this->outEntries = []; - } - - /** - * Count elements of an object. - * - * @see 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->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], $this->inputEntries[$entry])) { - $this->outEntries[$entry] = $this->inputEntries[$entry]; - - return true; - } - - return false; - } - - /** - * @param int $encryptionMethod - */ - public function setEncryptionMethod($encryptionMethod = ZipFile::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/ZipInputStream.php b/src/PhpZip/Stream/ZipInputStream.php deleted file mode 100644 index 6d1779d..0000000 --- a/src/PhpZip/Stream/ZipInputStream.php +++ /dev/null @@ -1,718 +0,0 @@ -in = $in; - } - - /** - * @throws ZipException - * - * @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 ( - $signature !== ZipEntry::LOCAL_FILE_HEADER_SIG - && $signature !== EndOfCentralDirectory::ZIP64_END_OF_CD_RECORD_SIG - && $signature !== EndOfCentralDirectory::END_OF_CD_SIG - ) { - throw new ZipException( - 'Expected Local File Header or (ZIP64) End Of Central Directory Record! Signature: ' . $signature - ); - } - } - - /** - * @throws ZipException - * - * @return EndOfCentralDirectory - */ - protected function readEndOfCentralDirectory() - { - if (!$this->findEndOfCentralDirectory()) { - throw new ZipException('Invalid zip file. The end of the central directory could not be found.'); - } - - $positionECD = ftell($this->in) - 4; - $buffer = fread($this->in, fstat($this->in)['size'] - $positionECD); - - $unpack = unpack( - 'vdiskNo/vcdDiskNo/vcdEntriesDisk/' . - 'vcdEntries/VcdSize/VcdPos/vcommentLength', - substr($buffer, 0, 18) - ); - - if ( - $unpack['diskNo'] !== 0 || - $unpack['cdDiskNo'] !== 0 || - $unpack['cdEntriesDisk'] !== $unpack['cdEntries'] - ) { - throw new ZipException( - 'ZIP file spanning/splitting is not supported!' - ); - } - // .ZIP file comment (variable sizeECD) - $comment = null; - - if ($unpack['commentLength'] > 0) { - $comment = substr($buffer, 18, $unpack['commentLength']); - } - - // Check for ZIP64 End Of Central Directory Locator exists. - $zip64ECDLocatorPosition = $positionECD - EndOfCentralDirectory::ZIP64_END_OF_CD_LOCATOR_LEN; - fseek($this->in, $zip64ECDLocatorPosition); - // zip64 end of central dir locator - // signature 4 bytes (0x07064b50) - if ($zip64ECDLocatorPosition > 0 && unpack( - 'V', - fread($this->in, 4) - )[1] === EndOfCentralDirectory::ZIP64_END_OF_CD_LOCATOR_SIG) { - $positionECD = $this->findZip64ECDPosition(); - $endCentralDirectory = $this->readZip64EndOfCentralDirectory($positionECD); - $endCentralDirectory->setComment($comment); - } else { - $endCentralDirectory = new EndOfCentralDirectory( - $unpack['cdEntries'], - $unpack['cdPos'], - $unpack['cdSize'], - false, - $comment - ); - } - - return $endCentralDirectory; - } - - /** - * @throws ZipException - * - * @return bool - */ - protected function findEndOfCentralDirectory() - { - $max = fstat($this->in)['size'] - EndOfCentralDirectory::END_OF_CENTRAL_DIRECTORY_RECORD_MIN_LEN; - - if ($max < 0) { - throw new ZipException('Too short to be a zip file'); - } - $min = $max >= 0xffff ? $max - 0xffff : 0; - // Search for End of central directory record. - for ($position = $max; $position >= $min; $position--) { - fseek($this->in, $position); - // end of central dir signature 4 bytes (0x06054b50) - if (unpack('V', fread($this->in, 4))[1] !== EndOfCentralDirectory::END_OF_CD_SIG) { - continue; - } - - return true; - } - - return false; - } - - /** - * Read Zip64 end of central directory locator and returns - * Zip64 end of central directory position. - * - * number of the disk with the - * start of the zip64 end of - * central directory 4 bytes - * relative offset of the zip64 - * end of central directory record 8 bytes - * total number of disks 4 bytes - * - * @throws ZipException - * - * @return int Zip64 End Of Central Directory position - */ - protected function findZip64ECDPosition() - { - $diskNo = unpack('V', fread($this->in, 4))[1]; - $zip64ECDPos = PackUtil::unpackLongLE(fread($this->in, 8)); - $totalDisks = unpack('V', fread($this->in, 4))[1]; - - if ($diskNo !== 0 || $totalDisks > 1) { - throw new ZipException('ZIP file spanning/splitting is not supported!'); - } - - return $zip64ECDPos; - } - - /** - * Read zip64 end of central directory locator and zip64 end - * of central directory record. - * - * zip64 end of central dir - * signature 4 bytes (0x06064b50) - * size of zip64 end of central - * directory record 8 bytes - * version made by 2 bytes - * version needed to extract 2 bytes - * number of this disk 4 bytes - * number of the disk with the - * start of the central directory 4 bytes - * total number of entries in the - * central directory on this disk 8 bytes - * total number of entries in the - * central directory 8 bytes - * size of the central directory 8 bytes - * offset of start of central - * directory with respect to - * the starting disk number 8 bytes - * zip64 extensible data sector (variable size) - * - * @param int $zip64ECDPosition - * - * @throws ZipException - * - * @return EndOfCentralDirectory - */ - protected function readZip64EndOfCentralDirectory($zip64ECDPosition) - { - fseek($this->in, $zip64ECDPosition); - - $buffer = fread($this->in, 56 /* zip64 end of cd rec length */); - - if (unpack('V', $buffer)[1] !== EndOfCentralDirectory::ZIP64_END_OF_CD_RECORD_SIG) { - throw new ZipException('Expected ZIP64 End Of Central Directory Record!'); - } - - $data = unpack( - 'VdiskNo/VcdDiskNo', - substr($buffer, 16) - ); - $cdEntriesDisk = PackUtil::unpackLongLE(substr($buffer, 24, 8)); - $entryCount = PackUtil::unpackLongLE(substr($buffer, 32, 8)); - $cdSize = PackUtil::unpackLongLE(substr($buffer, 40, 8)); - $cdPos = PackUtil::unpackLongLE(substr($buffer, 48, 8)); - - if ($data['diskNo'] !== 0 || $data['cdDiskNo'] !== 0 || $entryCount !== $cdEntriesDisk) { - throw new ZipException('ZIP file spanning/splitting is not supported!'); - } - - if ($entryCount < 0 || $entryCount > 0x7fffffff) { - throw new ZipException('Total Number Of Entries In The Central Directory out of range!'); - } - - // skip zip64 extensible data sector (variable sizeEndCD) - - return new EndOfCentralDirectory( - $entryCount, - $cdPos, - $cdSize, - true - ); - } - - /** - * Reads the central directory from the given seekable byte channel - * and populates the internal tables with ZipEntry instances. - * - * The ZipEntry's will know all data that can be obtained from the - * central directory alone, but not the data that requires the local - * file header or additional data to be read. - * - * @param EndOfCentralDirectory $endOfCentralDirectory - * - * @throws ZipException - * - * @return ZipEntry[] - */ - protected function mountCentralDirectory(EndOfCentralDirectory $endOfCentralDirectory) - { - $entries = []; - - fseek($this->in, $endOfCentralDirectory->getCdOffset()); - - if (!($cdStream = fopen('php://temp', 'w+b'))) { - throw new ZipException('Temp resource can not open from write'); - } - stream_copy_to_stream($this->in, $cdStream, $endOfCentralDirectory->getCdSize()); - rewind($cdStream); - for ($numEntries = $endOfCentralDirectory->getEntryCount(); $numEntries > 0; $numEntries--) { - $entry = $this->readCentralDirectoryEntry($cdStream); - $entries[$entry->getName()] = $entry; - } - fclose($cdStream); - - return $entries; - } - - /** - * Read central directory entry. - * - * central file header signature 4 bytes (0x02014b50) - * version made by 2 bytes - * version needed to extract 2 bytes - * general purpose bit flag 2 bytes - * compression method 2 bytes - * last mod file time 2 bytes - * last mod file date 2 bytes - * crc-32 4 bytes - * compressed size 4 bytes - * uncompressed size 4 bytes - * file name length 2 bytes - * extra field length 2 bytes - * file comment length 2 bytes - * disk number start 2 bytes - * internal file attributes 2 bytes - * external file attributes 4 bytes - * relative offset of local header 4 bytes - * - * file name (variable size) - * extra field (variable size) - * file comment (variable size) - * - * @param resource $stream - * - * @throws ZipException - * - * @return ZipEntry - */ - public function readCentralDirectoryEntry($stream) - { - if (unpack('V', fread($stream, 4))[1] !== ZipOutputStreamInterface::CENTRAL_FILE_HEADER_SIG) { - throw new ZipException('Corrupt zip file. Cannot read central dir entry.'); - } - - $data = unpack( - 'vversionMadeBy/vversionNeededToExtract/' . - 'vgeneralPurposeBitFlag/vcompressionMethod/' . - 'VlastModFile/Vcrc/VcompressedSize/' . - 'VuncompressedSize/vfileNameLength/vextraFieldLength/' . - 'vfileCommentLength/vdiskNumberStart/vinternalFileAttributes/' . - 'VexternalFileAttributes/VoffsetLocalHeader', - fread($stream, 42) - ); - - $createdOS = ($data['versionMadeBy'] & 0xFF00) >> 8; - $softwareVersion = $data['versionMadeBy'] & 0x00FF; - - $extractOS = ($data['versionNeededToExtract'] & 0xFF00) >> 8; - $extractVersion = $data['versionNeededToExtract'] & 0x00FF; - - $name = fread($stream, $data['fileNameLength']); - - $extra = ''; - - if ($data['extraFieldLength'] > 0) { - $extra = fread($stream, $data['extraFieldLength']); - } - - $comment = null; - - if ($data['fileCommentLength'] > 0) { - $comment = fread($stream, $data['fileCommentLength']); - } - - $entry = new ZipSourceEntry($this); - $entry->setName($name); - $entry->setCreatedOS($createdOS); - $entry->setSoftwareVersion($softwareVersion); - $entry->setVersionNeededToExtract($extractVersion); - $entry->setExtractedOS($extractOS); - $entry->setMethod($data['compressionMethod']); - $entry->setGeneralPurposeBitFlags($data['generalPurposeBitFlag']); - $entry->setDosTime($data['lastModFile']); - $entry->setCrc($data['crc']); - $entry->setCompressedSize($data['compressedSize']); - $entry->setSize($data['uncompressedSize']); - $entry->setInternalAttributes($data['internalFileAttributes']); - $entry->setExternalAttributes($data['externalFileAttributes']); - $entry->setOffset($data['offsetLocalHeader']); - $entry->setComment($comment); - $entry->setExtra($extra); - - return $entry; - } - - /** - * @param ZipEntry $entry - * - * @throws ZipException - * - * @return string - */ - public function readEntryContent(ZipEntry $entry) - { - if ($entry->isDirectory()) { - return null; - } - - if (!($entry instanceof ZipSourceEntry)) { - throw new InvalidArgumentException('entry must be ' . ZipSourceEntry::class); - } - $isEncrypted = $entry->isEncrypted(); - - if ($isEncrypted && $entry->getPassword() === null) { - throw new ZipException('Can not password from entry ' . $entry->getName()); - } - - $startPos = $pos = $entry->getOffset(); - - fseek($this->in, $startPos); - - // local file header signature 4 bytes (0x04034b50) - if (unpack('V', fread($this->in, 4))[1] !== ZipEntry::LOCAL_FILE_HEADER_SIG) { - throw new ZipException($entry->getName() . ' (expected Local File Header)'); - } - fseek($this->in, $pos + ZipEntry::LOCAL_FILE_HEADER_FILE_NAME_LENGTH_POS); - // file name length 2 bytes - // extra field length 2 bytes - $data = unpack('vfileLength/vextraLength', fread($this->in, 4)); - $pos += ZipEntry::LOCAL_FILE_HEADER_MIN_LEN + $data['fileLength'] + $data['extraLength']; - - if ($entry->getCrc() === ZipEntry::UNKNOWN) { - throw new ZipException(sprintf('Missing crc for entry %s', $entry->getName())); - } - - $method = $entry->getMethod(); - - fseek($this->in, $pos); - - // Get raw entry content - $compressedSize = $entry->getCompressedSize(); - $content = ''; - - if ($compressedSize > 0) { - $offset = 0; - - while ($offset < $compressedSize) { - $read = min(8192 /* chunk size */, $compressedSize - $offset); - $content .= fread($this->in, $read); - $offset += $read; - } - } - - $skipCheckCrc = false; - - if ($isEncrypted) { - if ($method === ZipEntry::METHOD_WINZIP_AES) { - // Strong Encryption Specification - WinZip AES - $winZipAesEngine = new WinZipAesEngine($entry); - $content = $winZipAesEngine->decrypt($content); - /** - * @var WinZipAesEntryExtraField $field - */ - $field = $entry->getExtraFieldsCollection()->get(WinZipAesEntryExtraField::getHeaderId()); - $method = $field->getMethod(); - $entry->setEncryptionMethod($field->getEncryptionMethod()); - $skipCheckCrc = true; - } else { - // Traditional PKWARE Decryption - $zipCryptoEngine = new TraditionalPkwareEncryptionEngine($entry); - $content = $zipCryptoEngine->decrypt($content); - $entry->setEncryptionMethod(ZipFile::ENCRYPTION_METHOD_TRADITIONAL); - } - - if (!$skipCheckCrc) { - // Check CRC32 in the Local File Header or Data Descriptor. - $localCrc = null; - - if ($entry->getGeneralPurposeBitFlag(ZipEntry::GPBF_DATA_DESCRIPTOR)) { - // The CRC32 is in the Data Descriptor after the compressed size. - // Note the Data Descriptor's Signature is optional: - // All newer apps should write it (and so does TrueVFS), - // but older apps might not. - fseek($this->in, $pos + $compressedSize); - $localCrc = unpack('V', fread($this->in, 4))[1]; - - if ($localCrc === ZipEntry::DATA_DESCRIPTOR_SIG) { - $localCrc = unpack('V', fread($this->in, 4))[1]; - } - } else { - fseek($this->in, $startPos + 14); - // The CRC32 in the Local File Header. - $localCrc = fread($this->in, 4)[1]; - } - - if (\PHP_INT_SIZE === 4) { - if (sprintf('%u', $entry->getCrc()) === sprintf('%u', $localCrc)) { - throw new Crc32Exception($entry->getName(), $entry->getCrc(), $localCrc); - } - } elseif ($localCrc !== $entry->getCrc()) { - throw new Crc32Exception($entry->getName(), $entry->getCrc(), $localCrc); - } - } - } - - switch ($method) { - case ZipFile::METHOD_STORED: - break; - - case ZipFile::METHOD_DEFLATED: - /** @noinspection PhpUsageOfSilenceOperatorInspection */ - $content = @gzinflate($content); - break; - - case ZipFile::METHOD_BZIP2: - if (!\extension_loaded('bz2')) { - throw new ZipException('Extension bzip2 not install'); - } - /** @noinspection PhpComposerExtensionStubsInspection */ - $content = bzdecompress($content); - - if (\is_int($content)) { // decompress error - $content = false; - } - break; - default: - throw new ZipUnsupportMethodException( - $entry->getName() . - ' (compression method ' . $method . ' is not supported)' - ); - } - - if ($content === false) { - if ($isEncrypted) { - throw new ZipAuthenticationException( - sprintf( - 'Invalid password for zip entry "%s"', - $entry->getName() - ) - ); - } - - throw new ZipException( - sprintf( - 'Failed to get the contents of the zip entry "%s"', - $entry->getName() - ) - ); - } - - if (!$skipCheckCrc) { - $localCrc = crc32($content); - - if (sprintf('%u', $entry->getCrc()) !== sprintf('%u', $localCrc)) { - if ($isEncrypted) { - throw new ZipAuthenticationException( - sprintf( - 'Invalid password for zip entry "%s"', - $entry->getName() - ) - ); - } - - throw new Crc32Exception($entry->getName(), $entry->getCrc(), $localCrc); - } - } - - return $content; - } - - /** - * @return resource - */ - public function getStream() - { - return $this->in; - } - - /** - * Copy the input stream of the LOC entry zip and the data into - * the output stream and zip the alignment if necessary. - * - * @param ZipEntry $entry - * @param ZipOutputStreamInterface $out - * - * @throws ZipException - */ - public function copyEntry(ZipEntry $entry, ZipOutputStreamInterface $out) - { - $pos = $entry->getOffset(); - - if ($pos === ZipEntry::UNKNOWN) { - throw new ZipException(sprintf('Missing local header offset for entry %s', $entry->getName())); - } - - $nameLength = \strlen($entry->getName()); - - fseek($this->in, $pos + ZipEntry::LOCAL_FILE_HEADER_MIN_LEN - 2, \SEEK_SET); - $sourceExtraLength = $destExtraLength = unpack('v', fread($this->in, 2))[1]; - - if ($sourceExtraLength > 0) { - // read Local File Header extra fields - fseek($this->in, $pos + ZipEntry::LOCAL_FILE_HEADER_MIN_LEN + $nameLength, \SEEK_SET); - $extra = ''; - $offset = 0; - - while ($offset < $sourceExtraLength) { - $read = min(8192 /* chunk size */, $sourceExtraLength - $offset); - $extra .= fread($this->in, $read); - $offset += $read; - } - $extraFieldsCollection = ExtraFieldsFactory::createExtraFieldCollections($extra, $entry); - - if (isset($extraFieldsCollection[ApkAlignmentExtraField::getHeaderId()]) && $this->zipModel->isZipAlign()) { - unset($extraFieldsCollection[ApkAlignmentExtraField::getHeaderId()]); - $destExtraLength = \strlen(ExtraFieldsFactory::createSerializedData($extraFieldsCollection)); - } - } else { - $extraFieldsCollection = new ExtraFieldsCollection(); - } - - $dataAlignmentMultiple = $this->zipModel->getZipAlign(); - $copyInToOutLength = $entry->getCompressedSize(); - - fseek($this->in, $pos, \SEEK_SET); - - if ( - $this->zipModel->isZipAlign() && - !$entry->isEncrypted() && - $entry->getMethod() === ZipFile::METHOD_STORED - ) { - if (StringUtil::endsWith($entry->getName(), '.so')) { - $dataAlignmentMultiple = ApkAlignmentExtraField::ANDROID_COMMON_PAGE_ALIGNMENT_BYTES; - } - - $dataMinStartOffset = - ftell($out->getStream()) + - ZipEntry::LOCAL_FILE_HEADER_MIN_LEN + - $destExtraLength + - $nameLength + - ApkAlignmentExtraField::ALIGNMENT_ZIP_EXTRA_MIN_SIZE_BYTES; - $padding = - ($dataAlignmentMultiple - ($dataMinStartOffset % $dataAlignmentMultiple)) - % $dataAlignmentMultiple; - - $alignExtra = new ApkAlignmentExtraField(); - $alignExtra->setMultiple($dataAlignmentMultiple); - $alignExtra->setPadding($padding); - $extraFieldsCollection->add($alignExtra); - - $extra = ExtraFieldsFactory::createSerializedData($extraFieldsCollection); - - // copy Local File Header without extra field length - // from input stream to output stream - stream_copy_to_stream($this->in, $out->getStream(), ZipEntry::LOCAL_FILE_HEADER_MIN_LEN - 2); - // write new extra field length (2 bytes) to output stream - fwrite($out->getStream(), pack('v', \strlen($extra))); - // skip 2 bytes to input stream - fseek($this->in, 2, \SEEK_CUR); - // copy name from input stream to output stream - stream_copy_to_stream($this->in, $out->getStream(), $nameLength); - // write extra field to output stream - fwrite($out->getStream(), $extra); - // skip source extraLength from input stream - fseek($this->in, $sourceExtraLength, \SEEK_CUR); - } else { - $copyInToOutLength += ZipEntry::LOCAL_FILE_HEADER_MIN_LEN + $sourceExtraLength + $nameLength; - } - - if ($entry->getGeneralPurposeBitFlag(ZipEntry::GPBF_DATA_DESCRIPTOR)) { -// crc-32 4 bytes -// compressed size 4 bytes -// uncompressed size 4 bytes - $copyInToOutLength += 12; - - if ($entry->isZip64ExtensionsRequired()) { -// compressed size +4 bytes -// uncompressed size +4 bytes - $copyInToOutLength += 8; - } - } - // copy loc, data, data descriptor from input to output stream - stream_copy_to_stream($this->in, $out->getStream(), $copyInToOutLength); - } - - /** - * @param ZipEntry $entry - * @param ZipOutputStreamInterface $out - */ - public function copyEntryData(ZipEntry $entry, ZipOutputStreamInterface $out) - { - $offset = $entry->getOffset(); - $nameLength = \strlen($entry->getName()); - - fseek($this->in, $offset + ZipEntry::LOCAL_FILE_HEADER_MIN_LEN - 2, \SEEK_SET); - $extraLength = unpack('v', fread($this->in, 2))[1]; - - fseek($this->in, $offset + ZipEntry::LOCAL_FILE_HEADER_MIN_LEN + $nameLength + $extraLength, \SEEK_SET); - // copy raw data from input stream to output stream - stream_copy_to_stream($this->in, $out->getStream(), $entry->getCompressedSize()); - } - - public function __destruct() - { - $this->close(); - } - - public function close() - { - if ($this->in !== null) { - fclose($this->in); - $this->in = null; - } - } -} diff --git a/src/PhpZip/Stream/ZipInputStreamInterface.php b/src/PhpZip/Stream/ZipInputStreamInterface.php deleted file mode 100644 index e4f2c9c..0000000 --- a/src/PhpZip/Stream/ZipInputStreamInterface.php +++ /dev/null @@ -1,63 +0,0 @@ -out = $out; - $this->zipModel = $zipModel; - } - - /** - * @throws ZipException - */ - public function writeZip() - { - $entries = $this->zipModel->getEntries(); - $outPosEntries = []; - - foreach ($entries as $entry) { - $outPosEntries[] = new OutputOffsetEntry(ftell($this->out), $entry); - $this->writeEntry($entry); - } - $centralDirectoryOffset = ftell($this->out); - - foreach ($outPosEntries as $outputEntry) { - $this->writeCentralDirectoryHeader($outputEntry); - } - $this->writeEndOfCentralDirectoryRecord($centralDirectoryOffset); - } - - /** - * @param ZipEntry $entry - * - * @throws ZipException - */ - public function writeEntry(ZipEntry $entry) - { - if ($entry instanceof ZipSourceEntry) { - $entry->getInputStream()->copyEntry($entry, $this); - - return; - } - - $entryContent = $this->entryCommitChangesAndReturnContent($entry); - - $offset = ftell($this->out); - $compressedSize = $entry->getCompressedSize(); - - $extra = $entry->getExtra(); - - $nameLength = \strlen($entry->getName()); - $extraLength = \strlen($extra); - - // zip align - if ( - $this->zipModel->isZipAlign() && - !$entry->isEncrypted() && - $entry->getMethod() === ZipFile::METHOD_STORED - ) { - $dataAlignmentMultiple = $this->zipModel->getZipAlign(); - - if (StringUtil::endsWith($entry->getName(), '.so')) { - $dataAlignmentMultiple = ApkAlignmentExtraField::ANDROID_COMMON_PAGE_ALIGNMENT_BYTES; - } - $dataMinStartOffset = - $offset + - ZipEntry::LOCAL_FILE_HEADER_MIN_LEN + - $extraLength + - $nameLength + - ApkAlignmentExtraField::ALIGNMENT_ZIP_EXTRA_MIN_SIZE_BYTES; - - $padding = - ($dataAlignmentMultiple - ($dataMinStartOffset % $dataAlignmentMultiple)) - % $dataAlignmentMultiple; - - $alignExtra = new ApkAlignmentExtraField(); - $alignExtra->setMultiple($dataAlignmentMultiple); - $alignExtra->setPadding($padding); - - $extraFieldsCollection = clone $entry->getExtraFieldsCollection(); - $extraFieldsCollection->add($alignExtra); - - $extra = ExtraFieldsFactory::createSerializedData($extraFieldsCollection); - $extraLength = \strlen($extra); - } - - $size = $nameLength + $extraLength; - - if ($size > 0xffff) { - throw new ZipException( - $entry->getName() . ' (the total size of ' . $size . - ' bytes for the name, extra fields and comment ' . - 'exceeds the maximum size of ' . 0xffff . ' bytes)' - ); - } - - $dd = $entry->isDataDescriptorRequired(); - fwrite( - $this->out, - pack( - 'VvvvVVVVvv', - // local file header signature 4 bytes (0x04034b50) - ZipEntry::LOCAL_FILE_HEADER_SIG, - // version needed to extract 2 bytes - ($entry->getExtractedOS() << 8) | $entry->getVersionNeededToExtract(), - // general purpose bit flag 2 bytes - $entry->getGeneralPurposeBitFlags(), - // compression method 2 bytes - $entry->getMethod(), - // last mod file time 2 bytes - // last mod file date 2 bytes - $entry->getDosTime(), - // crc-32 4 bytes - $dd ? 0 : $entry->getCrc(), - // compressed size 4 bytes - $dd ? 0 : $entry->getCompressedSize(), - // uncompressed size 4 bytes - $dd ? 0 : $entry->getSize(), - // file name length 2 bytes - $nameLength, - // extra field length 2 bytes - $extraLength - ) - ); - - if ($nameLength > 0) { - fwrite($this->out, $entry->getName()); - } - - if ($extraLength > 0) { - fwrite($this->out, $extra); - } - - if ($entry instanceof ZipChangesEntry && !$entry->isChangedContent()) { - $entry->getSourceEntry()->getInputStream()->copyEntryData($entry->getSourceEntry(), $this); - } elseif ($entryContent !== null) { - fwrite($this->out, $entryContent); - } - - if ($entry->getCrc() === ZipEntry::UNKNOWN) { - throw new ZipException(sprintf('No crc for entry %s', $entry->getName())); - } - - if ($entry->getSize() === ZipEntry::UNKNOWN) { - throw new ZipException(sprintf('No uncompressed size for entry %s', $entry->getName())); - } - - if ($entry->getCompressedSize() === ZipEntry::UNKNOWN) { - throw new ZipException(sprintf('No compressed size for entry %s', $entry->getName())); - } - - if ($entry->getGeneralPurposeBitFlag(ZipEntry::GPBF_DATA_DESCRIPTOR)) { - // data descriptor signature 4 bytes (0x08074b50) - // crc-32 4 bytes - fwrite($this->out, pack('VV', ZipEntry::DATA_DESCRIPTOR_SIG, $entry->getCrc())); - // compressed size 4 or 8 bytes - // uncompressed size 4 or 8 bytes - if ($entry->isZip64ExtensionsRequired()) { - fwrite($this->out, PackUtil::packLongLE($compressedSize)); - fwrite($this->out, PackUtil::packLongLE($entry->getSize())); - } else { - fwrite($this->out, pack('VV', $entry->getCompressedSize(), $entry->getSize())); - } - } elseif ($compressedSize !== $entry->getCompressedSize()) { - throw new ZipException( - $entry->getName() . ' (expected compressed entry size of ' - . $entry->getCompressedSize() . ' bytes, ' . - 'but is actually ' . $compressedSize . ' bytes)' - ); - } - } - - /** - * @param ZipEntry $entry - * - * @throws ZipException - * - * @return string|null - */ - protected function entryCommitChangesAndReturnContent(ZipEntry $entry) - { - if ($entry->getCreatedOS() === ZipEntry::UNKNOWN) { - $entry->setCreatedOS(ZipEntry::PLATFORM_UNIX); - } - - if ($entry->getSoftwareVersion() === ZipEntry::UNKNOWN) { - $entry->setSoftwareVersion(63); - } - - if ($entry->getExtractedOS() === ZipEntry::UNKNOWN) { - $entry->setExtractedOS(ZipEntry::PLATFORM_UNIX); - } - - if ($entry->getTime() === ZipEntry::UNKNOWN) { - $entry->setTime(time()); - } - $method = $entry->getMethod(); - - $encrypted = $entry->isEncrypted(); - // See appendix D of PKWARE's ZIP File Format Specification. - $utf8 = true; - - if ($encrypted && $entry->getPassword() === null) { - throw new ZipException(sprintf('Password not set for entry %s', $entry->getName())); - } - - // Compose General Purpose Bit Flag. - $general = ($encrypted ? ZipEntry::GPBF_ENCRYPTED : 0) - | ($entry->isDataDescriptorRequired() ? ZipEntry::GPBF_DATA_DESCRIPTOR : 0) - | ($utf8 ? ZipEntry::GPBF_UTF8 : 0); - - $entryContent = null; - $extraFieldsCollection = $entry->getExtraFieldsCollection(); - - if (!($entry instanceof ZipChangesEntry && !$entry->isChangedContent())) { - $entryContent = $entry->getEntryContent(); - - if ($entryContent !== null) { - $entry->setSize(\strlen($entryContent)); - $entry->setCrc(crc32($entryContent)); - - if ($encrypted && $method === ZipEntry::METHOD_WINZIP_AES) { - /** - * @var WinZipAesEntryExtraField $field - */ - $field = $extraFieldsCollection->get(WinZipAesEntryExtraField::getHeaderId()); - - if ($field !== null) { - $method = $field->getMethod(); - } - } - - switch ($method) { - case ZipFile::METHOD_STORED: - break; - - case ZipFile::METHOD_DEFLATED: - $entryContent = gzdeflate($entryContent, $entry->getCompressionLevel()); - break; - - case ZipFile::METHOD_BZIP2: - $compressionLevel = $entry->getCompressionLevel() === ZipFile::LEVEL_DEFAULT_COMPRESSION ? - ZipEntry::LEVEL_DEFAULT_BZIP2_COMPRESSION : - $entry->getCompressionLevel(); - /** @noinspection PhpComposerExtensionStubsInspection */ - $entryContent = bzcompress($entryContent, $compressionLevel); - - if (\is_int($entryContent)) { - throw new ZipException('Error bzip2 compress. Error code: ' . $entryContent); - } - break; - - case ZipEntry::UNKNOWN: - $entryContent = $this->determineBestCompressionMethod($entry, $entryContent); - $method = $entry->getMethod(); - break; - - default: - throw new ZipException($entry->getName() . ' (unsupported compression method ' . $method . ')'); - } - - if ($method === ZipFile::METHOD_DEFLATED) { - $bit1 = false; - $bit2 = false; - switch ($entry->getCompressionLevel()) { - case ZipFile::LEVEL_BEST_COMPRESSION: - $bit1 = true; - break; - - case ZipFile::LEVEL_FAST: - $bit2 = true; - break; - - case ZipFile::LEVEL_SUPER_FAST: - $bit1 = true; - $bit2 = true; - break; - } - - $general |= ($bit1 ? ZipEntry::GPBF_COMPRESSION_FLAG1 : 0); - $general |= ($bit2 ? ZipEntry::GPBF_COMPRESSION_FLAG2 : 0); - } - - if ($encrypted) { - if (\in_array( - $entry->getEncryptionMethod(), - [ - ZipFile::ENCRYPTION_METHOD_WINZIP_AES_128, - ZipFile::ENCRYPTION_METHOD_WINZIP_AES_192, - ZipFile::ENCRYPTION_METHOD_WINZIP_AES_256, - ], - true - )) { - $keyStrength = WinZipAesEntryExtraField::getKeyStrangeFromEncryptionMethod( - $entry->getEncryptionMethod() - ); // size bits - $field = ExtraFieldsFactory::createWinZipAesEntryExtra(); - $field->setKeyStrength($keyStrength); - $field->setMethod($method); - $size = $entry->getSize(); - - if ($size >= 20 && $method !== ZipFile::METHOD_BZIP2) { - $field->setVendorVersion(WinZipAesEntryExtraField::VV_AE_1); - } else { - $field->setVendorVersion(WinZipAesEntryExtraField::VV_AE_2); - $entry->setCrc(0); - } - $extraFieldsCollection->add($field); - $entry->setMethod(ZipEntry::METHOD_WINZIP_AES); - - $winZipAesEngine = new WinZipAesEngine($entry); - $entryContent = $winZipAesEngine->encrypt($entryContent); - } elseif ($entry->getEncryptionMethod() === ZipFile::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 - * - * @throws ZipException - * - * @return string - */ - protected function determineBestCompressionMethod(ZipEntry $entry, $content) - { - if ($content !== null) { - $entryContent = gzdeflate($content, $entry->getCompressionLevel()); - - if (\strlen($entryContent) < \strlen($content)) { - $entry->setMethod(ZipFile::METHOD_DEFLATED); - - return $entryContent; - } - $entry->setMethod(ZipFile::METHOD_STORED); - } - - return $content; - } - - /** - * Writes a Central File Header record. - * - * @param OutputOffsetEntry $outEntry - */ - protected function writeCentralDirectoryHeader(OutputOffsetEntry $outEntry) - { - $entry = $outEntry->getEntry(); - $compressedSize = $entry->getCompressedSize(); - $size = $entry->getSize(); - // This test MUST NOT include the CRC-32 because VV_AE_2 sets it to - // UNKNOWN! - if (($compressedSize | $size) === ZipEntry::UNKNOWN) { - throw new RuntimeException('invalid entry'); - } - $extra = $entry->getExtra(); - $extraSize = \strlen($extra); - - $commentLength = \strlen($entry->getComment()); - fwrite( - $this->out, - pack( - 'VvvvvVVVVvvvvvVV', - // central file header signature 4 bytes (0x02014b50) - self::CENTRAL_FILE_HEADER_SIG, - // version made by 2 bytes - ($entry->getCreatedOS() << 8) | $entry->getSoftwareVersion(), - // version needed to extract 2 bytes - ($entry->getExtractedOS() << 8) | $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 - $entry->getInternalAttributes(), - // external file attributes 4 bytes - $entry->getExternalAttributes(), - // relative offset of local header 4 bytes - $outEntry->getOffset() - ) - ); - // file name (variable size) - fwrite($this->out, $entry->getName()); - - if ($extraSize > 0) { - // extra field (variable size) - fwrite($this->out, $extra); - } - - if ($commentLength > 0) { - // file comment (variable size) - fwrite($this->out, $entry->getComment()); - } - } - - /** - * @param int $centralDirectoryOffset - */ - protected function writeEndOfCentralDirectoryRecord($centralDirectoryOffset) - { - $cdEntriesCount = \count($this->zipModel); - - $position = ftell($this->out); - $centralDirectorySize = $position - $centralDirectoryOffset; - - $cdEntriesZip64 = $cdEntriesCount > 0xFFFF; - $cdSizeZip64 = $centralDirectorySize > 0xFFFFFFFF; - $cdOffsetZip64 = $centralDirectoryOffset > 0xFFFFFFFF; - - $zip64Required = $cdEntriesZip64 || $cdSizeZip64 || $cdOffsetZip64; - - if ($zip64Required) { - $zip64EndOfCentralDirectoryOffset = ftell($this->out); - - // find max software version, version needed to extract and most common platform - list($softwareVersion, $versionNeededToExtract) = array_reduce( - $this->zipModel->getEntries(), - static function (array $carry, ZipEntry $entry) { - $carry[0] = max($carry[0], $entry->getSoftwareVersion() & 0xFF); - $carry[1] = max($carry[1], $entry->getVersionNeededToExtract() & 0xFF); - - return $carry; - }, - [10 /* simple file min ver */, 45 /* zip64 ext min ver */] - ); - - $createdOS = $extractedOS = ZipEntry::PLATFORM_FAT; - $versionMadeBy = ($createdOS << 8) | max($softwareVersion, 45 /* zip64 ext min ver */); - $versionExtractedBy = ($extractedOS << 8) | max($versionNeededToExtract, 45 /* zip64 ext min ver */); - - // signature 4 bytes (0x06064b50) - fwrite($this->out, pack('V', EndOfCentralDirectory::ZIP64_END_OF_CD_RECORD_SIG)); - // size of zip64 end of central - // directory record 8 bytes - fwrite($this->out, PackUtil::packLongLE(44)); - fwrite( - $this->out, - pack( - 'vvVV', - // version made by 2 bytes - $versionMadeBy & 0xFFFF, - // version needed to extract 2 bytes - $versionExtractedBy & 0xFFFF, - // number of this disk 4 bytes - 0, - // number of the disk with the - // start of the central directory 4 bytes - 0 - ) - ); - // total number of entries in the - // central directory on this disk 8 bytes - fwrite($this->out, PackUtil::packLongLE($cdEntriesCount)); - // total number of entries in the - // central directory 8 bytes - fwrite($this->out, PackUtil::packLongLE($cdEntriesCount)); - // 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)); - - // write zip64 end of central directory locator - fwrite( - $this->out, - pack( - 'VV', - // zip64 end of central dir locator - // signature 4 bytes (0x07064b50) - EndOfCentralDirectory::ZIP64_END_OF_CD_LOCATOR_SIG, - // number of the disk with the - // start of the zip64 end of - // central directory 4 bytes - 0 - ) - ); - // relative offset of the zip64 - // end of central directory record 8 bytes - fwrite($this->out, PackUtil::packLongLE($zip64EndOfCentralDirectoryOffset)); - // total number of disks 4 bytes - fwrite($this->out, pack('V', 1)); - } - - $comment = $this->zipModel->getArchiveComment(); - $commentLength = $comment !== null ? \strlen($comment) : 0; - - fwrite( - $this->out, - pack( - 'VvvvvVVv', - // end of central dir signature 4 bytes (0x06054b50) - EndOfCentralDirectory::END_OF_CD_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 - $cdEntriesZip64 ? 0xFFFF : $cdEntriesCount, - // total number of entries in - // the central directory 2 bytes - $cdEntriesZip64 ? 0xFFFF : $cdEntriesCount, - // size of the central directory 4 bytes - $cdSizeZip64 ? 0xFFFFFFFF : $centralDirectorySize, - // offset of start of central - // directory with respect to - // the starting disk number 4 bytes - $cdOffsetZip64 ? 0xFFFFFFFF : $centralDirectoryOffset, - // .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 deleted file mode 100644 index dc50801..0000000 --- a/src/PhpZip/Stream/ZipOutputStreamInterface.php +++ /dev/null @@ -1,29 +0,0 @@ - 3 && ctype_alpha($file[0]) + && $file[1] === ':' + && strspn($file, '/\\', 2, 1) + ) + || parse_url($file, \PHP_URL_SCHEME) !== null; + } + + /** + * @param string $linkPath + * @param string $target + * + * @return bool + */ + public static function symlink($target, $linkPath) + { + if (\DIRECTORY_SEPARATOR === '\\') { + $linkPath = str_replace('/', '\\', $linkPath); + $target = str_replace('/', '\\', $target); + $abs = null; + + if (!self::isAbsolutePath($target)) { + $abs = realpath(\dirname($linkPath) . \DIRECTORY_SEPARATOR . $target); + + if (\is_string($abs)) { + $target = $abs; + } + } + } + + if (!symlink($target, $linkPath)) { + if (\DIRECTORY_SEPARATOR === '\\' && is_file($target)) { + return copy($target, $linkPath); + } + + return false; + } + + return true; + } + + /** + * @param string $file + * + * @return bool + */ + public static function isBadCompressionFile($file) + { + $badCompressFileExt = [ + 'dic', + 'dng', + 'f4v', + 'flipchart', + 'h264', + 'lrf', + 'mobi', + 'mts', + 'nef', + 'pspimage', + ]; + + $ext = strtolower(pathinfo($file, \PATHINFO_EXTENSION)); + + if (\in_array($ext, $badCompressFileExt, true)) { + return true; + } + + $mimeType = self::getMimeTypeFromFile($file); + + return self::isBadCompressionMimeType($mimeType); + } + + /** + * @param string $mimeType + * + * @return bool + */ + public static function isBadCompressionMimeType($mimeType) + { + static $badDeflateCompMimeTypes = [ + 'application/epub+zip', + 'application/gzip', + 'application/vnd.debian.binary-package', + 'application/vnd.oasis.opendocument.graphics', + 'application/vnd.oasis.opendocument.presentation', + 'application/vnd.oasis.opendocument.text', + 'application/vnd.oasis.opendocument.text-master', + 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + 'application/vnd.rn-realmedia', + 'application/x-7z-compressed', + 'application/x-arj', + 'application/x-bzip2', + 'application/x-hwp', + 'application/x-lzip', + 'application/x-lzma', + 'application/x-ms-reader', + 'application/x-rar', + 'application/x-rpm', + 'application/x-stuffit', + 'application/x-tar', + 'application/x-xz', + 'application/zip', + 'application/zlib', + 'audio/flac', + 'audio/mpeg', + 'audio/ogg', + 'audio/vnd.dolby.dd-raw', + 'audio/webm', + 'audio/x-ape', + 'audio/x-hx-aac-adts', + 'audio/x-m4a', + 'audio/x-m4a', + 'audio/x-wav', + 'image/gif', + 'image/heic', + 'image/jp2', + 'image/jpeg', + 'image/png', + 'image/vnd.djvu', + 'image/webp', + 'image/x-canon-cr2', + 'video/ogg', + 'video/webm', + 'video/x-matroska', + 'video/x-ms-asf', + 'x-epoc/x-sisx-app', + ]; + + if (\in_array($mimeType, $badDeflateCompMimeTypes, true)) { + return true; + } + + return false; + } + + /** + * @param string $file + * + * @return string + * + * @noinspection PhpComposerExtensionStubsInspection + */ + public static function getMimeTypeFromFile($file) + { + if (\function_exists('mime_content_type')) { + return mime_content_type($file); + } + + return 'application/octet-stream'; + } + + /** + * @param string $contents + * + * @return string + * @noinspection PhpComposerExtensionStubsInspection + */ + public static function getMimeTypeFromString($contents) + { + $contents = (string) $contents; + $finfo = new \finfo(\FILEINFO_MIME); + $mimeType = $finfo->buffer($contents); + + if ($mimeType === false) { + $mimeType = 'application/octet-stream'; + } + + return explode(';', $mimeType)[0]; + } } diff --git a/src/PhpZip/Util/Iterator/IgnoreFilesFilterIterator.php b/src/Util/Iterator/IgnoreFilesFilterIterator.php similarity index 100% rename from src/PhpZip/Util/Iterator/IgnoreFilesFilterIterator.php rename to src/Util/Iterator/IgnoreFilesFilterIterator.php diff --git a/src/PhpZip/Util/Iterator/IgnoreFilesRecursiveFilterIterator.php b/src/Util/Iterator/IgnoreFilesRecursiveFilterIterator.php similarity index 96% rename from src/PhpZip/Util/Iterator/IgnoreFilesRecursiveFilterIterator.php rename to src/Util/Iterator/IgnoreFilesRecursiveFilterIterator.php index 580805b..8935127 100644 --- a/src/PhpZip/Util/Iterator/IgnoreFilesRecursiveFilterIterator.php +++ b/src/Util/Iterator/IgnoreFilesRecursiveFilterIterator.php @@ -66,7 +66,6 @@ class IgnoreFilesRecursiveFilterIterator extends \RecursiveFilterIterator /** * @return IgnoreFilesRecursiveFilterIterator - * @noinspection PhpMissingParentCallCommonInspection */ public function getChildren() { diff --git a/src/PhpZip/Util/PackUtil.php b/src/Util/PackUtil.php similarity index 84% rename from src/PhpZip/Util/PackUtil.php rename to src/Util/PackUtil.php index bd50ee2..653fab7 100644 --- a/src/PhpZip/Util/PackUtil.php +++ b/src/Util/PackUtil.php @@ -13,13 +13,13 @@ namespace PhpZip\Util; final class PackUtil { /** - * @param int|string $longValue + * @param int $longValue * * @return string */ public static function packLongLE($longValue) { - if (\PHP_INT_SIZE === 8 && \PHP_VERSION_ID >= 506030) { + if (\PHP_VERSION_ID >= 506030) { return pack('P', $longValue); } @@ -33,13 +33,13 @@ final class PackUtil } /** - * @param string|int $value + * @param string $value * * @return int */ public static function unpackLongLE($value) { - if (\PHP_INT_SIZE === 8 && \PHP_VERSION_ID >= 506030) { + if (\PHP_VERSION_ID >= 506030) { return unpack('P', $value)[1]; } $unpack = unpack('Va/Vb', $value); diff --git a/src/PhpZip/Util/StringUtil.php b/src/Util/StringUtil.php similarity index 65% rename from src/PhpZip/Util/StringUtil.php rename to src/Util/StringUtil.php index 8b067db..c40a8ad 100644 --- a/src/PhpZip/Util/StringUtil.php +++ b/src/Util/StringUtil.php @@ -31,4 +31,24 @@ final class StringUtil return $needle === '' || (($temp = \strlen($haystack) - \strlen($needle)) >= 0 && strpos($haystack, $needle, $temp) !== false); } + + /** + * @param string $string + * + * @return bool + */ + public static function isBinary($string) + { + return strpos($string, "\0") !== false; + } + + /** + * @param string $name + * + * @return bool + */ + public static function isASCII($name) + { + return preg_match('~[^\x20-\x7e]~', (string) $name) === 0; + } } diff --git a/src/PhpZip/ZipFile.php b/src/ZipFile.php similarity index 56% rename from src/PhpZip/ZipFile.php rename to src/ZipFile.php index 8b0a803..5b4f656 100644 --- a/src/PhpZip/ZipFile.php +++ b/src/ZipFile.php @@ -1,33 +1,39 @@ 'application/zip', 'apk' => 'application/vnd.android.package-archive', 'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', - 'jar' => 'application/java-archive', 'epub' => 'application/epub+zip', + 'jar' => 'application/java-archive', + 'odt' => 'application/vnd.oasis.opendocument.text', + 'pptx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.template', + 'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + 'xpi' => 'application/x-xpinstall', ]; - /** @var ZipInputStreamInterface input seekable input stream */ - protected $inputStream; + /** @var ZipContainer */ + protected $zipContainer; - /** @var ZipModel */ - protected $zipModel; + /** @var ZipReader|null */ + private $reader; /** * ZipFile constructor. */ public function __construct() { - $this->zipModel = new ZipModel(); + $this->zipContainer = new ZipContainer(); + } + + /** + * @param resource $inputStream + * @param array $options + * + * @return ZipReader + */ + protected function createZipReader($inputStream, array $options = []) + { + return new ZipReader($inputStream, $options); + } + + /** + * @return ZipWriter + */ + protected function createZipWriter() + { + return new ZipWriter($this->zipContainer); } /** * Open zip archive from file. * * @param string $filename + * @param array $options * * @throws ZipException if can't open file * * @return ZipFile */ - public function openFile($filename) + public function openFile($filename, array $options = []) { if (!file_exists($filename)) { throw new ZipException("File {$filename} does not exist."); @@ -94,21 +108,21 @@ class ZipFile implements ZipFileInterface if (!($handle = @fopen($filename, 'rb'))) { throw new ZipException("File {$filename} can't open."); } - $this->openFromStream($handle); - return $this; + return $this->openFromStream($handle, $options); } /** * Open zip archive from raw string data. * * @param string $data + * @param array $options * * @throws ZipException if can't open temp stream * * @return ZipFile */ - public function openFromString($data) + public function openFromString($data, array $options = []) { if ($data === null || $data === '') { throw new InvalidArgumentException('Empty string passed'); @@ -119,41 +133,24 @@ class ZipFile implements ZipFileInterface } fwrite($handle, $data); rewind($handle); - $this->openFromStream($handle); - return $this; + return $this->openFromStream($handle, $options); } /** * Open zip archive from stream resource. * * @param resource $handle + * @param array $options * * @throws ZipException * * @return ZipFile */ - public function openFromStream($handle) + public function openFromStream($handle, array $options = []) { - if (!\is_resource($handle)) { - throw new InvalidArgumentException('Invalid stream resource.'); - } - $type = get_resource_type($handle); - - if ($type !== 'stream') { - throw new InvalidArgumentException("Invalid resource type - {$type}."); - } - $meta = stream_get_meta_data($handle); - - if ($meta['stream_type'] === 'dir') { - throw new InvalidArgumentException("Invalid stream type - {$meta['stream_type']}."); - } - - if (!$meta['seekable']) { - throw new InvalidArgumentException('Resource cannot seekable stream.'); - } - $this->inputStream = new ZipInputStream($handle); - $this->zipModel = $this->inputStream->readZip(); + $this->reader = $this->createZipReader($handle, $options); + $this->zipContainer = new ZipContainer($this->reader->read()); return $this; } @@ -163,7 +160,8 @@ class ZipFile implements ZipFileInterface */ public function getListFiles() { - return array_map('strval', array_keys($this->zipModel->getEntries())); + // strval is needed to cast entry names to string type + return array_map('strval', array_keys($this->zipContainer->getEntries())); } /** @@ -171,7 +169,7 @@ class ZipFile implements ZipFileInterface */ public function count() { - return $this->zipModel->count(); + return $this->zipContainer->count(); } /** @@ -181,7 +179,7 @@ class ZipFile implements ZipFileInterface */ public function getArchiveComment() { - return $this->zipModel->getArchiveComment(); + return $this->zipContainer->getArchiveComment(); } /** @@ -193,11 +191,37 @@ class ZipFile implements ZipFileInterface */ public function setArchiveComment($comment = null) { - $this->zipModel->setArchiveComment($comment); + $this->zipContainer->setArchiveComment($comment); return $this; } + /** + * Checks if there is an entry in the archive. + * + * @param string $entryName + * + * @return bool + */ + public function hasEntry($entryName) + { + return $this->zipContainer->hasEntry($entryName); + } + + /** + * Returns ZipEntry object. + * + * @param string $entryName + * + * @throws ZipEntryNotFoundException + * + * @return ZipEntry + */ + public function getEntry($entryName) + { + return $this->zipContainer->getEntry($entryName); + } + /** * Checks that the entry in the archive is a directory. * Returns true if and only if this ZIP entry represents a directory entry @@ -211,7 +235,7 @@ class ZipFile implements ZipFileInterface */ public function isDirectory($entryName) { - return $this->zipModel->getEntry($entryName)->isDirectory(); + return $this->getEntry($entryName)->isDirectory(); } /** @@ -220,12 +244,13 @@ class ZipFile implements ZipFileInterface * @param string $entryName * * @throws ZipEntryNotFoundException + * @throws ZipException * * @return string */ public function getEntryComment($entryName) { - return $this->zipModel->getEntry($entryName)->getComment(); + return $this->getEntry($entryName)->getComment(); } /** @@ -241,7 +266,7 @@ class ZipFile implements ZipFileInterface */ public function setEntryComment($entryName, $comment = null) { - $this->zipModel->getEntryForChanges($entryName)->setComment($comment); + $this->getEntry($entryName)->setComment($comment); return $this; } @@ -252,24 +277,35 @@ class ZipFile implements ZipFileInterface * @param string $entryName * * @throws ZipException + * @throws ZipEntryNotFoundException * * @return string */ public function getEntryContents($entryName) { - return $this->zipModel->getEntry($entryName)->getEntryContent(); + $zipData = $this->zipContainer->getEntry($entryName)->getData(); + + if ($zipData === null) { + throw new ZipException(sprintf('No data for zip entry %s', $entryName)); + } + + return $zipData->getDataAsString(); } /** - * Checks if there is an entry in the archive. - * * @param string $entryName * - * @return bool + * @throws ZipException + * @throws ZipEntryNotFoundException + * + * @return resource */ - public function hasEntry($entryName) + public function getEntryStream($entryName) { - return $this->zipModel->hasEntry($entryName); + $resource = ZipEntryStreamWrapper::wrap($this->zipContainer->getEntry($entryName)); + rewind($resource); + + return $resource; } /** @@ -284,7 +320,7 @@ class ZipFile implements ZipFileInterface */ public function getEntryInfo($entryName) { - return new ZipInfo($this->zipModel->getEntry($entryName)); + return new ZipInfo($this->zipContainer->getEntry($entryName)); } /** @@ -294,7 +330,13 @@ class ZipFile implements ZipFileInterface */ public function getAllInfo() { - return array_map([$this, 'getEntryInfo'], $this->zipModel->getEntries()); + $infoMap = []; + + foreach ($this->zipContainer->getEntries() as $name => $entry) { + $infoMap[$name] = new ZipInfo($entry); + } + + return $infoMap; } /** @@ -302,37 +344,49 @@ class ZipFile implements ZipFileInterface */ public function matcher() { - return $this->zipModel->matcher(); + return $this->zipContainer->matcher(); } /** - * Extract the archive contents. + * Returns an array of zip records (ex. for modify time). + * + * @return ZipEntry[] array of raw zip entries + */ + public function getEntries() + { + return $this->zipContainer->getEntries(); + } + + /** + * Extract the archive contents (unzip). * * Extract the complete archive or the given files to the specified destination. * - * @param string $destination location where to extract the files - * @param array|string|null $entries The entries to extract. It accepts either - * a single entry name or an array of names. + * @param string $destDir 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. * * @throws ZipException * * @return ZipFile */ - public function extractTo($destination, $entries = null) + public function extractTo($destDir, $entries = null) { - if (!file_exists($destination)) { - throw new ZipException(sprintf('Destination %s not found', $destination)); + if (!file_exists($destDir)) { + throw new ZipException(sprintf('Destination %s not found', $destDir)); } - if (!is_dir($destination)) { + if (!is_dir($destDir)) { throw new ZipException('Destination is not directory'); } - if (!is_writable($destination)) { + if (!is_writable($destDir)) { throw new ZipException('Destination is not writable directory'); } - $zipEntries = $this->zipModel->getEntries(); + $extractedEntries = []; + + $zipEntries = $this->zipContainer->getEntries(); if (!empty($entries)) { if (\is_string($entries)) { @@ -344,165 +398,375 @@ class ZipFile implements ZipFileInterface $flipEntries = array_flip($entries); $zipEntries = array_filter( $zipEntries, - static function (ZipEntry $zipEntry) use ($flipEntries) { - return isset($flipEntries[$zipEntry->getName()]); - } + /** + * @param string $entryName + * + * @return bool + */ + static function ($entryName) use ($flipEntries) { + return isset($flipEntries[$entryName]); + }, + \ARRAY_FILTER_USE_KEY ); } } - foreach ($zipEntries as $entry) { - $entryName = FilesUtil::normalizeZipPath($entry->getName()); - $file = $destination . \DIRECTORY_SEPARATOR . $entryName; + if (empty($zipEntries)) { + return $this; + } + + /** @var int[] $lastModDirs */ + $lastModDirs = []; + + krsort($zipEntries, \SORT_NATURAL); + + $symlinks = []; + $destDir = rtrim($destDir, '/\\'); + + foreach ($zipEntries as $entryName => $entry) { + $unixMode = $entry->getUnixMode(); + $entryName = FilesUtil::normalizeZipPath($entryName); + $file = $destDir . \DIRECTORY_SEPARATOR . $entryName; + + if (\DIRECTORY_SEPARATOR === '\\') { + $file = str_replace('/', '\\', $file); + } + $extractedEntries[$file] = $entry; + $modifyTimestamp = $entry->getMTime()->getTimestamp(); + $atime = $entry->getATime(); + $accessTimestamp = $atime === null ? null : $atime->getTimestamp(); + + $dir = $entry->isDirectory() ? $file : \dirname($file); + + if (!is_dir($dir)) { + $dirMode = $entry->isDirectory() ? $unixMode : 0755; + + if ($dirMode === 0) { + $dirMode = 0755; + } + + if (!mkdir($dir, $dirMode, true) && !is_dir($dir)) { + throw new \RuntimeException(sprintf('Directory "%s" was not created', $dir)); + } + chmod($dir, $dirMode); + } + + $parts = explode('/', rtrim($entryName, '/')); + $path = $destDir . \DIRECTORY_SEPARATOR; + + foreach ($parts as $part) { + if (!isset($lastModDirs[$path]) || $lastModDirs[$path] > $modifyTimestamp) { + $lastModDirs[$path] = $modifyTimestamp; + } + + $path .= $part . \DIRECTORY_SEPARATOR; + } if ($entry->isDirectory()) { - if (!is_dir($file)) { - if (!mkdir($file, 0755, true) && !is_dir($file)) { - throw new ZipException('Can not create dir ' . $file); - } - chmod($file, 0755); - touch($file, $entry->getTime()); - } + $lastModDirs[$dir] = $modifyTimestamp; continue; } - $dir = \dirname($file); - if (!is_dir($dir)) { - if (!mkdir($dir, 0755, true) && !is_dir($dir)) { - throw new ZipException('Can not create dir ' . $dir); - } - chmod($dir, 0755); - touch($dir, $entry->getTime()); + $zipData = $entry->getData(); + + if ($zipData === null) { + continue; } - if (file_put_contents($file, $entry->getEntryContent()) === false) { - throw new ZipException('Can not extract file ' . $entry->getName()); + if ($entry->isUnixSymlink()) { + $symlinks[$file] = $zipData->getDataAsString(); + + continue; + } + + /** @noinspection PhpUsageOfSilenceOperatorInspection */ + if (!($handle = @fopen($file, 'w+b'))) { + throw new ZipException( + sprintf( + 'Cannot extract zip entry %s. File %s cannot open for write.', + $entry->getName(), + $file + ) + ); + } + + try { + $zipData->copyDataToStream($handle); + } catch (ZipException $e) { + unlink($file); + + throw $e; + } finally { + fclose($handle); + } + + if ($unixMode === 0) { + $unixMode = 0644; + } + chmod($file, $unixMode); + + if ($accessTimestamp !== null) { + /** @noinspection PotentialMalwareInspection */ + touch($file, $modifyTimestamp, $accessTimestamp); + } else { + touch($file, $modifyTimestamp); } - touch($file, $entry->getTime()); } + foreach ($symlinks as $linkPath => $target) { + if (!FilesUtil::symlink($target, $linkPath)) { + unset($extractedEntries[$linkPath]); + } + } + + krsort($lastModDirs, \SORT_NATURAL); + + foreach ($lastModDirs as $dir => $lastMod) { + touch($dir, $lastMod); + } + +// ksort($extractedEntries); + return $this; } /** * Add entry from the string. * - * @param string $localName zip entry name + * @param string $entryName zip entry name * @param string $contents string contents * @param int|null $compressionMethod Compression method. - * Use {@see ZipFile::METHOD_STORED}, {@see ZipFile::METHOD_DEFLATED} or - * {@see ZipFile::METHOD_BZIP2}. If null, then auto choosing method. + * Use {@see ZipCompressionMethod::STORED}, + * {@see ZipCompressionMethod::DEFLATED} or + * {@see ZipCompressionMethod::BZIP2}. + * If null, then auto choosing method. * * @throws ZipException * * @return ZipFile - * - * @see ZipFile::METHOD_STORED - * @see ZipFile::METHOD_DEFLATED - * @see ZipFile::METHOD_BZIP2 */ - public function addFromString($localName, $contents, $compressionMethod = null) + public function addFromString($entryName, $contents, $compressionMethod = null) { + if ($entryName === null) { + throw new InvalidArgumentException('Entry name is null'); + } + if ($contents === null) { throw new InvalidArgumentException('Contents is null'); } - if ($localName === null) { - throw new InvalidArgumentException('Entry name is null'); - } - $localName = ltrim((string) $localName, '\\/'); + $entryName = ltrim((string) $entryName, '\\/'); - if ($localName === '') { + if ($entryName === '') { throw new InvalidArgumentException('Empty entry name'); } $contents = (string) $contents; $length = \strlen($contents); - if ($compressionMethod === null) { - if ($length >= 512) { - $compressionMethod = ZipEntry::UNKNOWN; + if ($compressionMethod === null || $compressionMethod === ZipEntry::UNKNOWN) { + if ($length < 512) { + $compressionMethod = ZipCompressionMethod::STORED; } else { - $compressionMethod = self::METHOD_STORED; + $mimeType = FilesUtil::getMimeTypeFromString($contents); + $compressionMethod = FilesUtil::isBadCompressionMimeType($mimeType) ? + ZipCompressionMethod::STORED : + ZipCompressionMethod::DEFLATED; } - } elseif (!\in_array($compressionMethod, self::$allowCompressionMethods, true)) { - throw new ZipUnsupportMethodException('Unsupported compression method ' . $compressionMethod); } - $externalAttributes = 0100644 << 16; - $entry = new ZipNewEntry($contents); - $entry->setName($localName); - $entry->setMethod($compressionMethod); - $entry->setTime(time()); - $entry->setExternalAttributes($externalAttributes); + $zipEntry = new ZipEntry($entryName); + $zipEntry->setData(new ZipNewData($zipEntry, $contents)); + $zipEntry->setUncompressedSize($length); + $zipEntry->setCompressionMethod($compressionMethod); + $zipEntry->setCreatedOS(ZipPlatform::OS_UNIX); + $zipEntry->setExtractedOS(ZipPlatform::OS_UNIX); + $zipEntry->setUnixMode(010644); + $zipEntry->setTime(time()); - $this->zipModel->addEntry($entry); + $this->addZipEntry($zipEntry); return $this; } + /** + * @param Finder $finder + * @param array $options + * + * @throws ZipException + * + * @return ZipEntry[] + */ + public function addFromFinder(Finder $finder, array $options = []) + { + $defaultOptions = [ + ZipOptions::STORE_ONLY_FILES => false, + ZipOptions::COMPRESSION_METHOD => null, + ZipOptions::MODIFIED_TIME => null, + ]; + $options += $defaultOptions; + + if ($options[ZipOptions::STORE_ONLY_FILES]) { + $finder->files(); + } + + $entries = []; + + foreach ($finder as $fileInfo) { + if ($fileInfo->isReadable()) { + $entry = $this->addSplFile($fileInfo, null, $options); + $entries[$entry->getName()] = $entry; + } + } + + return $entries; + } + + /** + * @param \SplFileInfo $file + * @param string|null $entryName + * @param array $options + * + * @throws ZipException + * + * @return ZipEntry + */ + public function addSplFile(\SplFileInfo $file, $entryName = null, array $options = []) + { + if ($file instanceof \DirectoryIterator) { + throw new InvalidArgumentException('File should not be \DirectoryIterator.'); + } + $defaultOptions = [ + ZipOptions::COMPRESSION_METHOD => null, + ZipOptions::MODIFIED_TIME => null, + ]; + $options += $defaultOptions; + + if (!$file->isReadable()) { + throw new InvalidArgumentException(sprintf('File %s is not readable', $file->getPathname())); + } + + if ($entryName === null) { + if ($file instanceof SymfonySplFileInfo) { + $entryName = $file->getRelativePathname(); + } else { + $entryName = $file->getBasename(); + } + } + + $entryName = ltrim((string) $entryName, '\\/'); + + if ($entryName === '') { + throw new InvalidArgumentException('Empty entry name'); + } + + $entryName = $file->isDir() ? rtrim($entryName, '/\\') . '/' : $entryName; + + $zipEntry = new ZipEntry($entryName); + $zipData = null; + + if ($file->isFile()) { + if (isset($options[ZipOptions::COMPRESSION_METHOD])) { + $compressionMethod = $options[ZipOptions::COMPRESSION_METHOD]; + } elseif ($file->getSize() < 512) { + $compressionMethod = ZipCompressionMethod::STORED; + } else { + $compressionMethod = FilesUtil::isBadCompressionFile($file->getPathname()) ? + ZipCompressionMethod::STORED : + ZipCompressionMethod::DEFLATED; + } + + $zipEntry->setUncompressedSize($file->getSize()); + $zipEntry->setCompressionMethod($compressionMethod); + + $zipData = new ZipFileData($file); + } elseif ($file->isDir()) { + $zipEntry->setCompressionMethod(ZipCompressionMethod::STORED); + $zipEntry->setUncompressedSize(0); + $zipEntry->setCompressedSize(0); + $zipEntry->setCrc(0); + } elseif ($file->isLink()) { + $linkTarget = $file->getLinkTarget(); + $lengthLinkTarget = \strlen($linkTarget); + + $zipEntry->setCompressionMethod(ZipCompressionMethod::STORED); + $zipEntry->setUncompressedSize($lengthLinkTarget); + $zipEntry->setCompressedSize($lengthLinkTarget); + $zipEntry->setCrc(crc32($linkTarget)); + + $zipData = new ZipNewData($zipEntry, $linkTarget); + } + + $zipEntry->setCreatedOS(ZipPlatform::OS_UNIX); + $zipEntry->setExtractedOS(ZipPlatform::OS_UNIX); + $zipEntry->setUnixMode($file->getPerms()); + + $timestamp = null; + + if (isset($options[ZipOptions::MODIFIED_TIME])) { + $mtime = $options[ZipOptions::MODIFIED_TIME]; + + if ($mtime instanceof \DateTimeInterface) { + $timestamp = $mtime->getTimestamp(); + } elseif (is_numeric($mtime)) { + $timestamp = (int) $mtime; + } elseif (\is_string($mtime)) { + $timestamp = strtotime($mtime); + + if ($timestamp === false) { + $timestamp = null; + } + } + } + + if ($timestamp === null) { + $timestamp = $file->getMTime(); + } + + $zipEntry->setTime($timestamp); + $zipEntry->setData($zipData); + + $this->addZipEntry($zipEntry); + + return $zipEntry; + } + + /** + * @param ZipEntry $zipEntry + */ + protected function addZipEntry(ZipEntry $zipEntry) + { + $this->zipContainer->addEntry($zipEntry); + } + /** * Add entry from the file. * * @param string $filename destination file - * @param string|null $localName zip Entry name + * @param string|null $entryName zip Entry name * @param int|null $compressionMethod Compression method. - * Use {@see ZipFile::METHOD_STORED}, {@see ZipFile::METHOD_DEFLATED} or - * {@see ZipFile::METHOD_BZIP2}. If null, then auto choosing method. + * Use {@see ZipCompressionMethod::STORED}, + * {@see ZipCompressionMethod::DEFLATED} or + * {@see ZipCompressionMethod::BZIP2}. + * If null, then auto choosing method. * * @throws ZipException * * @return ZipFile - * - * @see ZipFile::METHOD_STORED - * @see ZipFile::METHOD_DEFLATED - * @see ZipFile::METHOD_BZIP2 */ - public function addFile($filename, $localName = null, $compressionMethod = null) + public function addFile($filename, $entryName = null, $compressionMethod = null) { - $entry = new ZipNewFileEntry($filename); - - if ($compressionMethod === null) { - if (\function_exists('mime_content_type')) { - /** @noinspection PhpComposerExtensionStubsInspection */ - $mimeType = @mime_content_type($filename); - $type = strtok($mimeType, '/'); - - if ($type === 'image') { - $compressionMethod = self::METHOD_STORED; - } elseif ($type === 'text' && filesize($filename) < 150) { - $compressionMethod = self::METHOD_STORED; - } else { - $compressionMethod = ZipEntry::UNKNOWN; - } - } elseif (filesize($filename) >= 512) { - $compressionMethod = ZipEntry::UNKNOWN; - } else { - $compressionMethod = self::METHOD_STORED; - } - } elseif (!\in_array($compressionMethod, self::$allowCompressionMethods, true)) { - throw new ZipUnsupportMethodException('Unsupported compression method ' . $compressionMethod); + if ($filename === null) { + throw new InvalidArgumentException('Filename is null'); } - if ($localName === null) { - $localName = basename($filename); - } - $localName = ltrim((string) $localName, '\\/'); - - if ($localName === '') { - throw new InvalidArgumentException('Empty entry name'); - } - - $stat = stat($filename); - $mode = sprintf('%o', $stat['mode']); - $externalAttributes = (octdec($mode) & 0xffff) << 16; - - $entry->setName($localName); - $entry->setMethod($compressionMethod); - $entry->setTime($stat['mtime']); - $entry->setExternalAttributes($externalAttributes); - - $this->zipModel->addEntry($entry); + $this->addSplFile( + new \SplFileInfo($filename), + $entryName, + [ + ZipOptions::COMPRESSION_METHOD => $compressionMethod, + ] + ); return $this; } @@ -511,63 +775,69 @@ class ZipFile implements ZipFileInterface * Add entry from the stream. * * @param resource $stream stream resource - * @param string $localName zip Entry name + * @param string $entryName zip Entry name * @param int|null $compressionMethod Compression method. - * Use {@see ZipFile::METHOD_STORED}, {@see ZipFile::METHOD_DEFLATED} or - * {@see ZipFile::METHOD_BZIP2}. If null, then auto choosing method. + * Use {@see ZipCompressionMethod::STORED}, + * {@see ZipCompressionMethod::DEFLATED} or + * {@see ZipCompressionMethod::BZIP2}. + * If null, then auto choosing method. * * @throws ZipException * * @return ZipFile - * - * @see ZipFile::METHOD_STORED - * @see ZipFile::METHOD_DEFLATED - * @see ZipFile::METHOD_BZIP2 */ - public function addFromStream($stream, $localName, $compressionMethod = null) + public function addFromStream($stream, $entryName, $compressionMethod = null) { if (!\is_resource($stream)) { throw new InvalidArgumentException('Stream is not resource'); } - if ($localName === null) { + if ($entryName === null) { throw new InvalidArgumentException('Entry name is null'); } - $localName = ltrim((string) $localName, '\\/'); + $entryName = ltrim((string) $entryName, '\\/'); - if ($localName === '') { + if ($entryName === '') { throw new InvalidArgumentException('Empty entry name'); } $fstat = fstat($stream); + $zipEntry = new ZipEntry($entryName); + if ($fstat !== false) { - $mode = sprintf('%o', $fstat['mode']); + $unixMode = (int) sprintf('%o', $fstat['mode']); $length = $fstat['size']; - if ($compressionMethod === null) { - if ($length >= 512) { - $compressionMethod = ZipEntry::UNKNOWN; + if ($compressionMethod === null || $compressionMethod === ZipEntry::UNKNOWN) { + if ($length < 512) { + $compressionMethod = ZipCompressionMethod::STORED; } else { - $compressionMethod = self::METHOD_STORED; + rewind($stream); + $bufferContents = stream_get_contents($stream, min(1024, $length)); + rewind($stream); + $mimeType = FilesUtil::getMimeTypeFromString($bufferContents); + $compressionMethod = FilesUtil::isBadCompressionMimeType($mimeType) ? + ZipCompressionMethod::STORED : + ZipCompressionMethod::DEFLATED; } + $zipEntry->setUncompressedSize($length); } } else { - $mode = 010644; + $unixMode = 010644; + + if ($compressionMethod === null || $compressionMethod === ZipEntry::UNKNOWN) { + $compressionMethod = ZipCompressionMethod::DEFLATED; + } } - if ($compressionMethod !== null && !\in_array($compressionMethod, self::$allowCompressionMethods, true)) { - throw new ZipUnsupportMethodException('Unsupported method ' . $compressionMethod); - } + $zipEntry->setCreatedOS(ZipPlatform::OS_UNIX); + $zipEntry->setExtractedOS(ZipPlatform::OS_UNIX); + $zipEntry->setUnixMode($unixMode); + $zipEntry->setCompressionMethod($compressionMethod); + $zipEntry->setTime(time()); + $zipEntry->setData(new ZipNewData($zipEntry, $stream)); - $externalAttributes = (octdec($mode) & 0xffff) << 16; - - $entry = new ZipNewEntry($stream); - $entry->setName($localName); - $entry->setMethod($compressionMethod); - $entry->setTime(time()); - $entry->setExternalAttributes($externalAttributes); - - $this->zipModel->addEntry($entry); + $this->addZipEntry($zipEntry); return $this; } @@ -592,18 +862,18 @@ class ZipFile implements ZipFileInterface throw new InvalidArgumentException('Empty dir name'); } $dirName = rtrim($dirName, '\\/') . '/'; - $externalAttributes = 040755 << 16; - $entry = new ZipNewEntry(); - $entry->setName($dirName); - $entry->setTime(time()); - $entry->setMethod(self::METHOD_STORED); - $entry->setSize(0); - $entry->setCompressedSize(0); - $entry->setCrc(0); - $entry->setExternalAttributes($externalAttributes); + $zipEntry = new ZipEntry($dirName); + $zipEntry->setCompressionMethod(ZipCompressionMethod::STORED); + $zipEntry->setUncompressedSize(0); + $zipEntry->setCompressedSize(0); + $zipEntry->setCrc(0); + $zipEntry->setCreatedOS(ZipPlatform::OS_UNIX); + $zipEntry->setExtractedOS(ZipPlatform::OS_UNIX); + $zipEntry->setUnixMode(040755); + $zipEntry->setTime(time()); - $this->zipModel->addEntry($entry); + $this->addZipEntry($zipEntry); return $this; } @@ -614,8 +884,10 @@ class ZipFile implements ZipFileInterface * @param string $inputDir Input directory * @param string $localPath add files to this directory, or the root * @param int|null $compressionMethod Compression method. - * Use {@see ZipFile::METHOD_STORED}, {@see ZipFile::METHOD_DEFLATED} or - * {@see ZipFile::METHOD_BZIP2}. If null, then auto choosing method. + * + * Use {@see ZipCompressionMethod::STORED}, {@see + * ZipCompressionMethod::DEFLATED} or + * {@see ZipCompressionMethod::BZIP2}. If null, then auto choosing method. * * @throws ZipException * @@ -648,16 +920,17 @@ class ZipFile implements ZipFileInterface * @param string $inputDir Input directory * @param string $localPath add files to this directory, or the root * @param int|null $compressionMethod Compression method. - * Use {@see ZipFile::METHOD_STORED}, {@see ZipFile::METHOD_DEFLATED} or - * {@see ZipFile::METHOD_BZIP2}. If null, then auto choosing method. + * Use {@see ZipCompressionMethod::STORED}, {@see + * ZipCompressionMethod::DEFLATED} or + * {@see ZipCompressionMethod::BZIP2}. If null, then auto choosing method. * * @throws ZipException * * @return ZipFile * - * @see ZipFile::METHOD_STORED - * @see ZipFile::METHOD_DEFLATED - * @see ZipFile::METHOD_BZIP2 + * @see ZipCompressionMethod::STORED + * @see ZipCompressionMethod::DEFLATED + * @see ZipCompressionMethod::BZIP2 */ public function addDirRecursive($inputDir, $localPath = '/', $compressionMethod = null) { @@ -686,16 +959,17 @@ class ZipFile implements ZipFileInterface * @param \Iterator $iterator directory iterator * @param string $localPath add files to this directory, or the root * @param int|null $compressionMethod Compression method. - * Use {@see ZipFile::METHOD_STORED}, {@see ZipFile::METHOD_DEFLATED} or - * {@see ZipFile::METHOD_BZIP2}. If null, then auto choosing method. + * Use {@see ZipCompressionMethod::STORED}, {@see + * ZipCompressionMethod::DEFLATED} or + * {@see ZipCompressionMethod::BZIP2}. If null, then auto choosing method. * * @throws ZipException * * @return ZipFile * - * @see ZipFile::METHOD_STORED - * @see ZipFile::METHOD_DEFLATED - * @see ZipFile::METHOD_BZIP2 + * @see ZipCompressionMethod::STORED + * @see ZipCompressionMethod::DEFLATED + * @see ZipCompressionMethod::BZIP2 */ public function addFilesFromIterator( \Iterator $iterator, @@ -748,12 +1022,13 @@ class ZipFile implements ZipFileInterface /** * Add files from glob pattern. * - * @param string $inputDir Input directory - * @param string $globPattern glob pattern - * @param string|null $localPath add files to this directory, or the root - * @param int|null $compressionMethod Compression method. - * Use {@see ZipFile::METHOD_STORED}, {@see ZipFile::METHOD_DEFLATED} or - * {@see ZipFile::METHOD_BZIP2}. If null, then auto choosing method. + * @param string $inputDir Input directory + * @param string $globPattern glob pattern + * @param string $localPath add files to this directory, or the root + * @param int|null $compressionMethod Compression method. + * Use {@see ZipCompressionMethod::STORED}, + * {@see ZipCompressionMethod::DEFLATED} or + * {@see ZipCompressionMethod::BZIP2}. If null, then auto choosing method. * * @throws ZipException * @@ -768,13 +1043,14 @@ class ZipFile implements ZipFileInterface /** * Add files from glob pattern. * - * @param string $inputDir Input directory - * @param string $globPattern glob pattern - * @param string|null $localPath add files to this directory, or the root - * @param bool $recursive recursive search - * @param int|null $compressionMethod Compression method. - * Use {@see ZipFile::METHOD_STORED}, {@see ZipFile::METHOD_DEFLATED} or - * {@see ZipFile::METHOD_BZIP2}. If null, then auto choosing method. + * @param string $inputDir Input directory + * @param string $globPattern glob pattern + * @param string $localPath add files to this directory, or the root + * @param bool $recursive recursive search + * @param int|null $compressionMethod Compression method. + * Use {@see ZipCompressionMethod::STORED}, + * {@see ZipCompressionMethod::DEFLATED} or + * {@see ZipCompressionMethod::BZIP2}. If null, then auto choosing method. * * @throws ZipException * @@ -823,12 +1099,13 @@ class ZipFile implements ZipFileInterface /** * 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 {@see ZipFile::METHOD_STORED}, {@see ZipFile::METHOD_DEFLATED} or - * {@see ZipFile::METHOD_BZIP2}. If null, then auto choosing method. + * @param string $inputDir Input directory + * @param string $globPattern glob pattern + * @param string $localPath add files to this directory, or the root + * @param int|null $compressionMethod Compression method. + * Use {@see ZipCompressionMethod::STORED}, + * {@see ZipCompressionMethod::DEFLATED} or + * {@see ZipCompressionMethod::BZIP2}. If null, then auto choosing method. * * @throws ZipException * @@ -843,12 +1120,13 @@ class ZipFile implements ZipFileInterface /** * Add files from regex pattern. * - * @param string $inputDir search files in this directory - * @param string $regexPattern regex pattern - * @param string|null $localPath add files to this directory, or the root - * @param int|null $compressionMethod Compression method. - * Use {@see ZipFile::METHOD_STORED}, {@see ZipFile::METHOD_DEFLATED} or - * {@see ZipFile::METHOD_BZIP2}. If null, then auto choosing method. + * @param string $inputDir search files in this directory + * @param string $regexPattern regex pattern + * @param string $localPath add files to this directory, or the root + * @param int|null $compressionMethod Compression method. + * Use {@see ZipCompressionMethod::STORED}, + * {@see ZipCompressionMethod::DEFLATED} or + * {@see ZipCompressionMethod::BZIP2}. If null, then auto choosing method. * * @throws ZipException * @@ -864,13 +1142,15 @@ class ZipFile implements ZipFileInterface /** * Add files from regex pattern. * - * @param string $inputDir search files in this directory - * @param string $regexPattern regex pattern - * @param string|null $localPath add files to this directory, or the root - * @param bool $recursive recursive search - * @param int|null $compressionMethod Compression method. - * Use {@see ZipFile::METHOD_STORED}, {@see ZipFile::METHOD_DEFLATED} or - * {@see ZipFile::METHOD_BZIP2}. If null, then auto choosing method. + * @param string $inputDir search files in this directory + * @param string $regexPattern regex pattern + * @param string $localPath add files to this directory, or the root + * @param bool $recursive recursive search + * @param int|null $compressionMethod Compression method. + * Use {@see ZipCompressionMethod::STORED}, + * {@see ZipCompressionMethod::DEFLATED} or + * {@see ZipCompressionMethod::BZIP2}. + * If null, then auto choosing method. * * @throws ZipException * @@ -946,12 +1226,13 @@ class ZipFile implements ZipFileInterface /** * 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 {@see ZipFile::METHOD_STORED}, {@see ZipFile::METHOD_DEFLATED} or - * {@see ZipFile::METHOD_BZIP2}. If null, then auto choosing method. + * @param string $inputDir search files in this directory + * @param string $regexPattern regex pattern + * @param string $localPath add files to this directory, or the root + * @param int|null $compressionMethod Compression method. + * Use {@see ZipCompressionMethod::STORED}, + * {@see ZipCompressionMethod::DEFLATED} or + * {@see ZipCompressionMethod::BZIP2}. If null, then auto choosing method. * * @throws ZipException * @@ -997,7 +1278,7 @@ class ZipFile implements ZipFileInterface $newName = ltrim((string) $newName, '\\/'); if ($oldName !== $newName) { - $this->zipModel->renameEntry($oldName, $newName); + $this->zipContainer->renameEntry($oldName, $newName); } return $this; @@ -1016,7 +1297,7 @@ class ZipFile implements ZipFileInterface { $entryName = ltrim((string) $entryName, '\\/'); - if (!$this->zipModel->deleteEntry($entryName)) { + if (!$this->zipContainer->deleteEntry($entryName)) { throw new ZipEntryNotFoundException($entryName); } @@ -1066,7 +1347,7 @@ class ZipFile implements ZipFileInterface */ public function deleteAll() { - $this->zipModel->deleteAll(); + $this->zipContainer->deleteAll(); return $this; } @@ -1078,22 +1359,15 @@ class ZipFile implements ZipFileInterface * * @return ZipFile * - * @see ZipFile::LEVEL_DEFAULT_COMPRESSION - * @see ZipFile::LEVEL_SUPER_FAST - * @see ZipFile::LEVEL_FAST - * @see ZipFile::LEVEL_BEST_COMPRESSION + * @see ZipCompressionLevel::NORMAL + * @see ZipCompressionLevel::SUPER_FAST + * @see ZipCompressionLevel::FAST + * @see ZipCompressionLevel::MAXIMUM */ public function setCompressionLevel($compressionLevel = self::LEVEL_DEFAULT_COMPRESSION) { - if ($compressionLevel < self::LEVEL_DEFAULT_COMPRESSION || - $compressionLevel > self::LEVEL_BEST_COMPRESSION - ) { - throw new InvalidArgumentException( - 'Invalid compression level. Minimum level ' . - self::LEVEL_DEFAULT_COMPRESSION . '. Maximum level ' . self::LEVEL_BEST_COMPRESSION - ); - } $this->matcher()->all()->invoke( + /** @param string $entry */ function ($entry) use ($compressionLevel) { $this->setCompressionLevelEntry($entry, $compressionLevel); } @@ -1110,29 +1384,15 @@ class ZipFile implements ZipFileInterface * * @return ZipFile * - * @see ZipFile::LEVEL_DEFAULT_COMPRESSION - * @see ZipFile::LEVEL_SUPER_FAST - * @see ZipFile::LEVEL_FAST - * @see ZipFile::LEVEL_BEST_COMPRESSION + * @see ZipCompressionLevel::NORMAL + * @see ZipCompressionLevel::SUPER_FAST + * @see ZipCompressionLevel::FAST + * @see ZipCompressionLevel::MAXIMUM */ public function setCompressionLevelEntry($entryName, $compressionLevel) { - if ($compressionLevel !== null) { - if ($compressionLevel < self::LEVEL_DEFAULT_COMPRESSION || - $compressionLevel > self::LEVEL_BEST_COMPRESSION - ) { - throw new InvalidArgumentException( - 'Invalid compression level. Minimum level ' . - self::LEVEL_DEFAULT_COMPRESSION . '. Maximum level ' . self::LEVEL_BEST_COMPRESSION - ); - } - $entry = $this->zipModel->getEntry($entryName); - - if ($compressionLevel !== $entry->getCompressionLevel()) { - $entry = $this->zipModel->getEntryForChanges($entry); - $entry->setCompressionLevel($compressionLevel); - } - } + $compressionLevel = (int) $compressionLevel; + $this->getEntry($entryName)->setCompressionLevel($compressionLevel); return $this; } @@ -1140,30 +1400,24 @@ class ZipFile implements ZipFileInterface /** * @param string $entryName * @param int $compressionMethod Compression method. - * Use {@see ZipFile::METHOD_STORED}, {@see ZipFile::METHOD_DEFLATED} or - * {@see ZipFile::METHOD_BZIP2}. If null, then auto choosing method. + * Use {@see ZipCompressionMethod::STORED}, {@see ZipCompressionMethod::DEFLATED} + * or + * {@see ZipCompressionMethod::BZIP2}. If null, then auto choosing method. * * @throws ZipException * * @return ZipFile * - * @see ZipFile::METHOD_STORED - * @see ZipFile::METHOD_DEFLATED - * @see ZipFile::METHOD_BZIP2 + * @see ZipCompressionMethod::STORED + * @see ZipCompressionMethod::DEFLATED + * @see ZipCompressionMethod::BZIP2 */ public function setCompressionMethodEntry($entryName, $compressionMethod) { - if (!\in_array($compressionMethod, self::$allowCompressionMethods, true)) { - throw new ZipUnsupportMethodException('Unsupported method ' . $compressionMethod); - } - $entry = $this->zipModel->getEntry($entryName); - - if ($compressionMethod !== $entry->getMethod()) { - $this->zipModel - ->getEntryForChanges($entry) - ->setMethod($compressionMethod) - ; - } + $this->zipContainer + ->getEntry($entryName) + ->setCompressionMethod($compressionMethod) + ; return $this; } @@ -1179,7 +1433,7 @@ class ZipFile implements ZipFileInterface */ public function setZipAlign($align = null) { - $this->zipModel->setZipAlign($align); + $this->zipContainer->setZipAlign($align); return $this; } @@ -1189,29 +1443,11 @@ class ZipFile implements ZipFileInterface * * @param string $password Password * - * @throws ZipException - * - * @return ZipFile - * - * @deprecated using ZipFile::setReadPassword() - */ - public function withReadPassword($password) - { - return $this->setReadPassword($password); - } - - /** - * Set password to all input encrypted entries. - * - * @param string $password Password - * - * @throws ZipException - * * @return ZipFile */ public function setReadPassword($password) { - $this->zipModel->setReadPassword($password); + $this->zipContainer->setReadPassword($password); return $this; } @@ -1228,47 +1464,25 @@ class ZipFile implements ZipFileInterface */ public function setReadPasswordEntry($entryName, $password) { - $this->zipModel->setReadPasswordEntry($entryName, $password); + $this->zipContainer->setReadPasswordEntry($entryName, $password); return $this; } - /** - * Set password for all entries for update. - * - * @param string $password If password null then encryption clear - * @param int|null $encryptionMethod Encryption method - * - * @throws ZipException - * - * @return ZipFile - * - * @deprecated using ZipFile::setPassword() - */ - public function withNewPassword($password, $encryptionMethod = self::ENCRYPTION_METHOD_WINZIP_AES_256) - { - return $this->setPassword($password, $encryptionMethod); - } - /** * Sets a new password for all files in the archive. * - * @param string $password + * @param string $password Password * @param int|null $encryptionMethod Encryption method * - * @throws ZipException - * * @return ZipFile */ - public function setPassword($password, $encryptionMethod = self::ENCRYPTION_METHOD_WINZIP_AES_256) + public function setPassword($password, $encryptionMethod = ZipEncryptionMethod::WINZIP_AES_256) { - $this->zipModel->setWritePassword($password); + $this->zipContainer->setWritePassword($password); if ($encryptionMethod !== null) { - if (!\in_array($encryptionMethod, self::$allowEncryptionMethods, true)) { - throw new ZipException('Invalid encryption method "' . $encryptionMethod . '"'); - } - $this->zipModel->setEncryptionMethod($encryptionMethod); + $this->zipContainer->setEncryptionMethod($encryptionMethod); } return $this; @@ -1287,26 +1501,11 @@ class ZipFile implements ZipFileInterface */ public function setPasswordEntry($entryName, $password, $encryptionMethod = null) { - if ($encryptionMethod !== null && !\in_array($encryptionMethod, self::$allowEncryptionMethods, true)) { - throw new ZipException('Invalid encryption method "' . $encryptionMethod . '"'); - } - $this->matcher()->add($entryName)->setPassword($password, $encryptionMethod); + $this->getEntry($entryName)->setPassword($password, $encryptionMethod); return $this; } - /** - * Remove password for all entries for update. - * - * @return ZipFile - * - * @deprecated using ZipFile::disableEncryption() - */ - public function withoutPassword() - { - return $this->disableEncryption(); - } - /** * Disable encryption for all entries that are already in the archive. * @@ -1314,7 +1513,7 @@ class ZipFile implements ZipFileInterface */ public function disableEncryption() { - $this->zipModel->removePassword(); + $this->zipContainer->removePassword(); return $this; } @@ -1328,7 +1527,7 @@ class ZipFile implements ZipFileInterface */ public function disableEncryptionEntry($entryName) { - $this->zipModel->removePasswordEntry($entryName); + $this->zipContainer->removePasswordEntry($entryName); return $this; } @@ -1340,7 +1539,7 @@ class ZipFile implements ZipFileInterface */ public function unchangeAll() { - $this->zipModel->unchangeAll(); + $this->zipContainer->unchangeAll(); return $this; } @@ -1352,7 +1551,7 @@ class ZipFile implements ZipFileInterface */ public function unchangeArchiveComment() { - $this->zipModel->unchangeArchiveComment(); + $this->zipContainer->unchangeArchiveComment(); return $this; } @@ -1366,7 +1565,7 @@ class ZipFile implements ZipFileInterface */ public function unchangeEntry($entry) { - $this->zipModel->unchangeEntry($entry); + $this->zipContainer->unchangeEntry($entry); return $this; } @@ -1519,11 +1718,16 @@ class ZipFile implements ZipFileInterface } $stream = new ResponseStream($handle); + $size = $stream->getSize(); + + if ($size !== null) { + /** @noinspection CallableParameterUseCaseInTypeContextInspection */ + $response = $response->withHeader('Content-Length', (string) $size); + } return $response ->withHeader('Content-Type', $mimeType) ->withHeader('Content-Disposition', $contentDispositionValue) - ->withHeader('Content-Length', $stream->getSize()) ->withBody($stream) ; } @@ -1537,8 +1741,7 @@ class ZipFile implements ZipFileInterface { $this->onBeforeSave(); - $output = new ZipOutputStream($handle, $this->zipModel); - $output->writeZip(); + $this->createZipWriter()->write($handle); } /** @@ -1575,10 +1778,10 @@ class ZipFile implements ZipFileInterface */ public function close() { - if ($this->inputStream !== null) { - $this->inputStream->close(); - $this->inputStream = null; - $this->zipModel = new ZipModel(); + if ($this->reader !== null) { + $this->reader->close(); + $this->reader = null; + $this->zipContainer = new ZipContainer(); } } @@ -1591,31 +1794,30 @@ class ZipFile implements ZipFileInterface */ public function rewrite() { - if ($this->inputStream === null) { + if ($this->reader === null) { throw new ZipException('input stream is null'); } - $meta = stream_get_meta_data($this->inputStream->getStream()); - $content = $this->outputAsString(); - $this->close(); - if ($meta['wrapper_type'] === 'plainfile') { - /** - * @var resource $uri - */ - $uri = $meta['uri']; + $meta = $this->reader->getStreamMetaData(); - if (file_put_contents($uri, $content) === false) { - throw new ZipException("Can not overwrite the zip file in the {$uri} file."); + if ($meta['wrapper_type'] === 'plainfile' && isset($meta['uri'])) { + $this->saveAsFile($meta['uri']); + $this->close(); + + if (!($handle = @fopen($meta['uri'], 'rb'))) { + throw new ZipException("File {$meta['uri']} can't open."); } + } else { + $handle = @fopen('php://temp', 'r+b'); - if (!($handle = @fopen($uri, 'rb'))) { - throw new ZipException("File {$uri} can't open."); + if (!$handle) { + throw new ZipException('php://temp cannot open for write.'); } - - return $this->openFromStream($handle); + $this->writeZipToStream($handle); + $this->close(); } - return $this->openFromString($content); + return $this->openFromStream($handle); } /** @@ -1631,8 +1833,8 @@ class ZipFile implements ZipFileInterface * * @see http://php.net/manual/en/arrayaccess.offsetset.php * - * @param string $entryName the offset to assign the value to - * @param mixed $contents the value to set + * @param string $entryName the offset to assign the value to + * @param string|\DirectoryIterator|\SplFileInfo|resource $contents the value to set * * @throws ZipException * @@ -1644,26 +1846,19 @@ class ZipFile implements ZipFileInterface public function offsetSet($entryName, $contents) { if ($entryName === null) { - throw new InvalidArgumentException('entryName is null'); + throw new InvalidArgumentException('Key must not be null, but must contain the name of the zip entry.'); } $entryName = ltrim((string) $entryName, '\\/'); if ($entryName === '') { - throw new InvalidArgumentException('entryName is empty'); + throw new InvalidArgumentException('Key is empty, but must contain the name of the zip entry.'); } - if ($contents instanceof \SplFileInfo) { - if ($contents instanceof \DirectoryIterator) { - $this->addFilesFromIterator($contents, $entryName); - - return; - } - $this->addFile($contents->getPathname(), $entryName); - - return; - } - - if (StringUtil::endsWith($entryName, '/')) { + if ($contents instanceof \DirectoryIterator) { + $this->addFilesFromIterator($contents, $entryName); + } elseif ($contents instanceof \SplFileInfo) { + $this->addSplFile($contents, $entryName); + } elseif (StringUtil::endsWith($entryName, '/')) { $this->addEmptyDir($entryName); } elseif (\is_resource($contents)) { $this->addFromStream($contents, $entryName); @@ -1729,7 +1924,7 @@ class ZipFile implements ZipFileInterface */ public function key() { - return key($this->zipModel->getEntries()); + return key($this->zipContainer->getEntries()); } /** @@ -1740,7 +1935,7 @@ class ZipFile implements ZipFileInterface */ public function next() { - next($this->zipModel->getEntries()); + next($this->zipContainer->getEntries()); } /** @@ -1781,6 +1976,6 @@ class ZipFile implements ZipFileInterface */ public function rewind() { - reset($this->zipModel->getEntries()); + reset($this->zipContainer->getEntries()); } } diff --git a/src/ZipFileInterface.php b/src/ZipFileInterface.php new file mode 100644 index 0000000..9ff0f60 --- /dev/null +++ b/src/ZipFileInterface.php @@ -0,0 +1,897 @@ +fp = @fopen($uri, $mode); + + return $this->fp !== false; + } + + /** + * Read from stream. + * + * This method is called in response to {@see fread()} and {@see fgets()}. + * + * Note: Remember to update the read/write position of the stream + * (by the number of bytes that were successfully read). + * + * @param int $count how many bytes of data from the current + * position should be returned + * + * @return false|string If there are less than count bytes available, + * return as many as are available. If no more data + * is available, return either FALSE or + * an empty string. + * + * @see https://www.php.net/streamwrapper.stream-read + */ + public function stream_read($count) + { + return fread($this->fp, $count); + } + + /** + * Seeks to specific location in a stream. + * + * This method is called in response to {@see fseek()}. + * The read/write position of the stream should be updated according + * to the offset and whence. + * + * @param int $offset the stream offset to seek to + * @param int $whence Possible values: + * {@see \SEEK_SET} - Set position equal to offset bytes. + * {@see \SEEK_CUR} - Set position to current location plus offset. + * {@see \SEEK_END} - Set position to end-of-file plus offset. + * + * @return bool return TRUE if the position was updated, FALSE otherwise + * + * @see https://www.php.net/streamwrapper.stream-seek + */ + public function stream_seek($offset, $whence = \SEEK_SET) + { + return fseek($this->fp, $offset, $whence) === 0; + } + + /** + * Retrieve the current position of a stream. + * + * This method is called in response to {@see fseek()} to determine + * the current position. + * + * @return int should return the current position of the stream + * + * @see https://www.php.net/streamwrapper.stream-tell + */ + public function stream_tell() + { + $pos = ftell($this->fp); + + if ($pos === false) { + throw new \RuntimeException('Cannot get stream position.'); + } + + return $pos; + } + + /** + * Tests for end-of-file on a file pointer. + * + * This method is called in response to {@see feof()}. + * + * @return bool should return TRUE if the read/write position is at + * the end of the stream and if no more data is available + * to be read, or FALSE otherwise + * + * @see https://www.php.net/streamwrapper.stream-eof + */ + public function stream_eof() + { + return feof($this->fp); + } + + /** + * Retrieve information about a file resource. + * + * This method is called in response to {@see fstat()}. + * + * @return array + * + * @see https://www.php.net/streamwrapper.stream-stat + * @see https://www.php.net/stat + * @see https://www.php.net/fstat + */ + public function stream_stat() + { + return fstat($this->fp); + } + + /** + * Flushes the output. + * + * This method is called in response to {@see fflush()} and when the + * stream is being closed while any unflushed data has been written to + * it before. + * If you have cached data in your stream but not yet stored it into + * the underlying storage, you should do so now. + * + * @return bool should return TRUE if the cached data was successfully + * stored (or if there was no data to store), or FALSE + * if the data could not be stored + * + * @see https://www.php.net/streamwrapper.stream-flush + */ + public function stream_flush() + { + return fflush($this->fp); + } + + /** + * Truncate stream. + * + * Will respond to truncation, e.g., through {@see ftruncate()}. + * + * @param int $new_size the new size + * + * @return bool returns TRUE on success or FALSE on failure + * + * @see https://www.php.net/streamwrapper.stream-truncate + */ + public function stream_truncate($new_size) + { + return ftruncate($this->fp, (int) $new_size); + } + + /** + * Write to stream. + * + * This method is called in response to {@see fwrite().} + * + * Note: Remember to update the current position of the stream by + * number of bytes that were successfully written. + * + * @param string $data should be stored into the underlying stream + * + * @return int should return the number of bytes that were successfully stored, or 0 if none could be stored + * + * @see https://www.php.net/streamwrapper.stream-write + */ + public function stream_write($data) + { + $bytes = fwrite($this->fp, $data); + + return $bytes === false ? 0 : $bytes; + } + + /** + * Close a resource. + * + * This method is called in response to {@see fclose()}. + * All resources that were locked, or allocated, by the wrapper should be released. + * + * @see https://www.php.net/streamwrapper.stream-close + */ + public function stream_close() + { + fclose($this->fp); + } +} diff --git a/tests/PhpZip/Internal/ZipFileExtended.php b/tests/Internal/ZipFileExtended.php similarity index 80% rename from tests/PhpZip/Internal/ZipFileExtended.php rename to tests/Internal/ZipFileExtended.php index 5e00777..4e4ff14 100644 --- a/tests/PhpZip/Internal/ZipFileExtended.php +++ b/tests/Internal/ZipFileExtended.php @@ -1,6 +1,6 @@ setZipAlign(4); $this->deleteFromRegex('~^META\-INF/~i'); } } diff --git a/tests/PhpZip/Issue24Test.php b/tests/Issue24Test.php similarity index 76% rename from tests/PhpZip/Issue24Test.php rename to tests/Issue24Test.php index 4250470..6aa59a6 100644 --- a/tests/PhpZip/Issue24Test.php +++ b/tests/Issue24Test.php @@ -1,8 +1,10 @@ outputFilename); - $stream = fopen('dummyfs://localhost/' . $this->outputFilename, 'rb'); + $uri = self::PROTO_DUMMYFS . '://localhost/' . $this->outputFilename; + $stream = fopen($uri, 'rb'); static::assertNotFalse($stream); $zip->openFromStream($stream); static::assertSame($zip->getListFiles(), ['file.txt']); diff --git a/tests/PhpZip/Internal/DummyFileSystemStream.php b/tests/PhpZip/Internal/DummyFileSystemStream.php deleted file mode 100644 index a278dcd..0000000 --- a/tests/PhpZip/Internal/DummyFileSystemStream.php +++ /dev/null @@ -1,72 +0,0 @@ -fp = fopen($path, $mode); - - return true; - } - - /** - * @param $count - * - * @return false|string - */ - public function stream_read($count) - { - return fread($this->fp, $count); - } - - /** - * @return false|int - */ - public function stream_tell() - { - return ftell($this->fp); - } - - /** - * @return bool - */ - public function stream_eof() - { - return feof($this->fp); - } - - /** - * @param $offset - * @param $whence - */ - public function stream_seek($offset, $whence) - { - fseek($this->fp, $offset, $whence); - } - - /** - * @return array - */ - public function stream_stat() - { - return fstat($this->fp); - } -} diff --git a/tests/PhpZip/PhpZipExtResourceTest.php b/tests/PhpZipExtResourceTest.php similarity index 88% rename from tests/PhpZip/PhpZipExtResourceTest.php rename to tests/PhpZipExtResourceTest.php index 859ebe5..cd6eaef 100644 --- a/tests/PhpZip/PhpZipExtResourceTest.php +++ b/tests/PhpZipExtResourceTest.php @@ -1,11 +1,12 @@ openFile($filename); @@ -48,7 +49,7 @@ class PhpZipExtResourceTest extends ZipTestCase */ public function testBug8009() { - $filename = __DIR__ . '/php-zip-ext-test-resources/bug8009.zip'; + $filename = __DIR__ . '/resources/pecl/bug8009.zip'; $zipFile = new ZipFile(); $zipFile->openFile($filename); @@ -95,7 +96,7 @@ class PhpZipExtResourceTest extends ZipTestCase public function provideBug40228() { return [ - [__DIR__ . '/php-zip-ext-test-resources/bug40228.zip'], + [__DIR__ . '/resources/pecl/bug40228.zip'], ]; } @@ -110,7 +111,7 @@ class PhpZipExtResourceTest extends ZipTestCase { $this->setExpectedException(Crc32Exception::class, 'file1'); - $filename = __DIR__ . '/php-zip-ext-test-resources/bug49072.zip'; + $filename = __DIR__ . '/resources/pecl/bug49072.zip'; $zipFile = new ZipFile(); $zipFile->openFile($filename); @@ -134,11 +135,11 @@ class PhpZipExtResourceTest extends ZipTestCase } else { // php 64 bit $this->setExpectedException( ZipAuthenticationException::class, - 'nvalid password for zip entry "bug70752.txt"' + 'Invalid password' ); } - $filename = __DIR__ . '/php-zip-ext-test-resources/bug70752.zip'; + $filename = __DIR__ . '/resources/pecl/bug70752.zip'; static::assertTrue(mkdir($this->outputDirname, 0755, true)); @@ -151,9 +152,10 @@ class PhpZipExtResourceTest extends ZipTestCase static::markTestIncomplete('failed test'); } catch (ZipException $exception) { static::assertFileNotExists($this->outputDirname . '/bug70752.txt'); - $zipFile->close(); throw $exception; + } finally { + $zipFile->close(); } } @@ -166,9 +168,9 @@ class PhpZipExtResourceTest extends ZipTestCase */ public function testPecl12414() { - $this->setExpectedException(ZipException::class, 'Corrupt zip file. Cannot read central dir entry.'); + $this->setExpectedException(ZipException::class, 'Corrupt zip file. Cannot read zip entry.'); - $filename = __DIR__ . '/php-zip-ext-test-resources/pecl12414.zip'; + $filename = __DIR__ . '/resources/pecl/pecl12414.zip'; $zipFile = new ZipFile(); $zipFile->openFile($filename); diff --git a/tests/SlowTests/Zip64Test.php b/tests/SlowTests/Zip64Test.php new file mode 100644 index 0000000..e262413 --- /dev/null +++ b/tests/SlowTests/Zip64Test.php @@ -0,0 +1,100 @@ +outputFilename); + $tmpLargeFile = $basedir . '/large_bin_file.bin'; + + $sizeLargeBinFile = (int) (4.2 * 1024 * 1024 * 1024); + $needFreeSpace = $sizeLargeBinFile * 4; + $diskFreeSpace = disk_free_space($basedir); + + if ($needFreeSpace > $diskFreeSpace) { + static::markTestIncomplete( + sprintf( + 'Not enough disk space for the test. Need to free %s', + FilesUtil::humanSize($needFreeSpace - $diskFreeSpace) + ) + ); + + return; + } + + try { + $commandCreateLargeBinFile = 'fallocate -l ' . escapeshellarg($sizeLargeBinFile) . ' ' . escapeshellarg($tmpLargeFile); + + exec($commandCreateLargeBinFile, $output, $returnCode); + + if ($returnCode !== 0) { + static::markTestIncomplete('Cannot create large file. Error code: ' . $returnCode); + + return; + } + + $zipFile = new ZipFile(); + $zipFile + ->addFile($tmpLargeFile, 'large_file1.bin', ZipCompressionMethod::STORED) + ->addFile($tmpLargeFile, 'large_file2.bin', ZipCompressionMethod::DEFLATED) + ->saveAsFile($this->outputFilename) + ->close() + ; + + if (is_file($tmpLargeFile)) { + unlink($tmpLargeFile); + } + + self::assertCorrectZipArchive($this->outputFilename); + + if (!is_dir($this->outputDirname)) { + mkdir($this->outputDirname, 0755, true); + } + + $zipFile->openFile($this->outputFilename); + $zipFile->extractTo($this->outputDirname); + + static::assertTrue(is_file($this->outputDirname . '/large_file1.bin')); + static::assertTrue(is_file($this->outputDirname . '/large_file2.bin')); + + $zipFile->deleteFromName('large_file1.bin'); + $zipFile->saveAsFile($this->outputFilename); + + self::assertCorrectZipArchive($this->outputFilename); + } finally { + if (is_file($tmpLargeFile)) { + unlink($tmpLargeFile); + } + } + } +} diff --git a/tests/PhpZip/Zip64Test.php b/tests/Zip64Test.php similarity index 88% rename from tests/PhpZip/Zip64Test.php rename to tests/Zip64Test.php index e9ebe85..bcb12cb 100644 --- a/tests/PhpZip/Zip64Test.php +++ b/tests/Zip64Test.php @@ -1,13 +1,16 @@ addFromString( 'entry' . $i . '.txt', random_bytes(mt_rand(100, 4096)), - ZipFile::METHOD_STORED + ZipCompressionMethod::STORED ); } $zipFile->saveAsFile($this->outputFilename); @@ -96,7 +98,7 @@ class ZipAlignTest extends ZipTestCase $zipFile->addFromString( 'entry' . $i . '.txt', random_bytes(mt_rand(100, 4096)), - ZipFile::METHOD_STORED + ZipCompressionMethod::STORED ); } $zipFile->setZipAlign(4); @@ -125,7 +127,7 @@ class ZipAlignTest extends ZipTestCase $zipFile->addFromString( 'entry' . $i . '.txt', random_bytes(mt_rand(100, 4096)), - ZipFile::METHOD_STORED + ZipCompressionMethod::STORED ); } $zipFile->saveAsFile($this->outputFilename); @@ -151,8 +153,8 @@ class ZipAlignTest extends ZipTestCase 'entry_new_' . ($isStored ? 'stored' : 'deflated') . '_' . $i . '.txt', random_bytes(mt_rand(100, 4096)), $isStored ? - ZipFile::METHOD_STORED : - ZipFile::METHOD_DEFLATED + ZipCompressionMethod::STORED : + ZipCompressionMethod::DEFLATED ); } $zipFile->setZipAlign(4); diff --git a/tests/PhpZip/ZipEventTest.php b/tests/ZipEventTest.php similarity index 82% rename from tests/PhpZip/ZipEventTest.php rename to tests/ZipEventTest.php index 950a645..63cd426 100644 --- a/tests/PhpZip/ZipEventTest.php +++ b/tests/ZipEventTest.php @@ -1,8 +1,9 @@ openFile(__DIR__ . '/resources/apk.zip'); static::assertTrue(isset($zipFile['META-INF/MANIFEST.MF'])); static::assertTrue(isset($zipFile['META-INF/CERT.SF'])); static::assertTrue(isset($zipFile['META-INF/CERT.RSA'])); + // the "META-INF/" folder will be deleted when saved + // in the ZipFileExtended::onBeforeSave() method $zipFile->saveAsFile($this->outputFilename); static::assertFalse(isset($zipFile['META-INF/MANIFEST.MF'])); static::assertFalse(isset($zipFile['META-INF/CERT.SF'])); @@ -28,11 +31,6 @@ class ZipEventTest extends ZipTestCase $zipFile->close(); static::assertCorrectZipArchive($this->outputFilename); - $result = static::assertVerifyZipAlign($this->outputFilename); - - if ($result !== null) { - static::assertTrue($result); - } $zipFile->openFile($this->outputFilename); static::assertFalse(isset($zipFile['META-INF/MANIFEST.MF'])); diff --git a/tests/PhpZip/ZipFileAddDirTest.php b/tests/ZipFileAddDirTest.php similarity index 81% rename from tests/PhpZip/ZipFileAddDirTest.php rename to tests/ZipFileAddDirTest.php index 9a08fb4..4c8285f 100644 --- a/tests/PhpZip/ZipFileAddDirTest.php +++ b/tests/ZipFileAddDirTest.php @@ -1,10 +1,11 @@ 'Hidden file', - 'text file.txt' => 'Text file', - 'Текстовый документ.txt' => 'Текстовый документ', - 'empty dir/' => null, - 'empty dir2/ещё пустой каталог/' => null, - 'catalog/New File' => 'New Catalog File', - 'catalog/New File 2' => 'New Catalog File 2', - 'catalog/Empty Dir/' => null, - 'category/list.txt' => 'Category list', - 'category/Pictures/128x160/Car/01.jpg' => 'File 01.jpg', - 'category/Pictures/128x160/Car/02.jpg' => 'File 02.jpg', - 'category/Pictures/240x320/Car/01.jpg' => 'File 01.jpg', - 'category/Pictures/240x320/Car/02.jpg' => 'File 02.jpg', - ]; - - /** - * Before test. - */ - protected function setUp() - { - parent::setUp(); - $this->fillDirectory(); - } - - protected function fillDirectory() - { - foreach (self::$files as $name => $content) { - $fullName = $this->outputDirname . '/' . $name; - - if ($content === null) { - if (!is_dir($fullName)) { - mkdir($fullName, 0755, true); - } - } else { - $dirname = \dirname($fullName); - - if (!is_dir($dirname)) { - mkdir($dirname, 0755, true); - } - file_put_contents($fullName, $content); - } - } - } - - /** - * @param ZipFile $zipFile - * @param array $actualResultFiles - * @param string $localPath - */ - protected static function assertFilesResult( - ZipFile $zipFile, - array $actualResultFiles = [], - $localPath = '/' - ) { - $localPath = rtrim($localPath, '/'); - $localPath = empty($localPath) ? '' : $localPath . '/'; - static::assertCount(\count($zipFile), $actualResultFiles); - $actualResultFiles = array_flip($actualResultFiles); - - foreach (self::$files as $file => $content) { - $zipEntryName = $localPath . $file; - - if (isset($actualResultFiles[$file])) { - static::assertTrue(isset($zipFile[$zipEntryName])); - static::assertSame($zipFile[$zipEntryName], $content); - unset($actualResultFiles[$file]); - } else { - static::assertFalse(isset($zipFile[$zipEntryName])); - } - } - static::assertEmpty($actualResultFiles); - } - /** * @throws ZipException */ @@ -111,6 +38,7 @@ class ZipFileAddDirTest extends ZipTestCase 'text file.txt', 'Текстовый документ.txt', 'empty dir/', + 'LoremIpsum.txt', ], $localPath ); @@ -137,6 +65,7 @@ class ZipFileAddDirTest extends ZipTestCase 'text file.txt', 'Текстовый документ.txt', 'empty dir/', + 'LoremIpsum.txt', ] ); $zipFile->close(); @@ -166,6 +95,7 @@ class ZipFileAddDirTest extends ZipTestCase 'text file.txt', 'Текстовый документ.txt', 'empty dir/', + 'LoremIpsum.txt', ], $localPath ); @@ -196,6 +126,7 @@ class ZipFileAddDirTest extends ZipTestCase 'text file.txt', 'Текстовый документ.txt', 'empty dir/', + 'LoremIpsum.txt', ] ); $zipFile->close(); @@ -267,6 +198,7 @@ class ZipFileAddDirTest extends ZipTestCase $ignoreFiles = [ 'Текстовый документ.txt', 'empty dir/', + 'LoremIpsum.txt', ]; $directoryIterator = new \DirectoryIterator($this->outputDirname); @@ -302,6 +234,7 @@ class ZipFileAddDirTest extends ZipTestCase 'empty dir2/ещё пустой каталог/', 'list.txt', 'category/Pictures/240x320', + 'LoremIpsum.txt', ]; $directoryIterator = new \RecursiveDirectoryIterator($this->outputDirname); @@ -354,6 +287,7 @@ class ZipFileAddDirTest extends ZipTestCase [ 'text file.txt', 'Текстовый документ.txt', + 'LoremIpsum.txt', ], $localPath ); @@ -387,6 +321,7 @@ class ZipFileAddDirTest extends ZipTestCase 'category/Pictures/128x160/Car/02.jpg', 'category/Pictures/240x320/Car/01.jpg', 'category/Pictures/240x320/Car/02.jpg', + 'LoremIpsum.txt', ], $localPath ); @@ -415,6 +350,7 @@ class ZipFileAddDirTest extends ZipTestCase [ 'text file.txt', 'Текстовый документ.txt', + 'LoremIpsum.txt', ], $localPath ); @@ -444,6 +380,7 @@ class ZipFileAddDirTest extends ZipTestCase 'text file.txt', 'Текстовый документ.txt', 'category/list.txt', + 'LoremIpsum.txt', 'category/Pictures/128x160/Car/01.jpg', 'category/Pictures/128x160/Car/02.jpg', 'category/Pictures/240x320/Car/01.jpg', diff --git a/tests/ZipFileSetTestCase.php b/tests/ZipFileSetTestCase.php new file mode 100644 index 0000000..6872840 --- /dev/null +++ b/tests/ZipFileSetTestCase.php @@ -0,0 +1,91 @@ + 'Hidden file', + 'text file.txt' => 'Text file', + 'Текстовый документ.txt' => 'Текстовый документ', + 'LoremIpsum.txt' => 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.', + 'empty dir/' => '', + 'empty dir2/ещё пустой каталог/' => '', + 'catalog/New File' => 'New Catalog File', + 'catalog/New File 2' => 'New Catalog File 2', + 'catalog/Empty Dir/' => '', + 'category/list.txt' => 'Category list', + 'category/Pictures/128x160/Car/01.jpg' => 'File 01.jpg', + 'category/Pictures/128x160/Car/02.jpg' => 'File 02.jpg', + 'category/Pictures/240x320/Car/01.jpg' => 'File 01.jpg', + 'category/Pictures/240x320/Car/02.jpg' => 'File 02.jpg', + ]; + + /** + * Before test. + */ + protected function setUp() + { + parent::setUp(); + $this->fillDirectory(); + } + + protected function fillDirectory() + { + foreach (self::$files as $name => $content) { + $fullName = $this->outputDirname . '/' . $name; + + if (StringUtil::endsWith($name, '/')) { + if (!is_dir($fullName) && !mkdir($fullName, 0755, true) && !is_dir($fullName)) { + throw new \RuntimeException(sprintf('Directory "%s" was not created', $fullName)); + } + } else { + $dirname = \dirname($fullName); + + if (!is_dir($dirname) && !mkdir($dirname, 0755, true) && !is_dir($dirname)) { + throw new \RuntimeException(sprintf('Directory "%s" was not created', $dirname)); + } + file_put_contents($fullName, $content); + } + } + } + + /** + * @param ZipFile $zipFile + * @param array $actualResultFiles + * @param string $localPath + */ + protected static function assertFilesResult( + ZipFile $zipFile, + array $actualResultFiles = [], + $localPath = '/' + ) { + $localPath = rtrim($localPath, '/'); + $localPath = empty($localPath) ? '' : $localPath . '/'; + static::assertCount(\count($zipFile), $actualResultFiles); + $actualResultFiles = array_flip($actualResultFiles); + + foreach (self::$files as $file => $content) { + $zipEntryName = $localPath . $file; + + if (isset($actualResultFiles[$file])) { + static::assertTrue(isset($zipFile[$zipEntryName])); + static::assertSame( + $zipFile[$zipEntryName], + $content, + sprintf('The content of the entry "%s" is not as expected.', $zipEntryName) + ); + unset($actualResultFiles[$file]); + } else { + static::assertFalse(isset($zipFile[$zipEntryName])); + } + } + static::assertEmpty($actualResultFiles); + } +} diff --git a/tests/PhpZip/ZipFileTest.php b/tests/ZipFileTest.php similarity index 87% rename from tests/PhpZip/ZipFileTest.php rename to tests/ZipFileTest.php index 6f90730..a701c48 100644 --- a/tests/PhpZip/ZipFileTest.php +++ b/tests/ZipFileTest.php @@ -1,7 +1,10 @@ setExpectedException(ZipException::class, 'can\'t open'); - /** @noinspection PhpComposerExtensionStubsInspection */ - if (posix_getuid() === 0) { - static::markTestSkipped('Skip the test for a user with root privileges'); - + if (static::skipTestForRootUser()) { return; } @@ -58,7 +59,7 @@ class ZipFileTest extends ZipTestCase */ public function testOpenFileEmptyFile() { - $this->setExpectedException(ZipException::class, 'Invalid zip file'); + $this->setExpectedException(ZipException::class, 'Corrupt zip file'); static::assertNotFalse(touch($this->outputFilename)); $zipFile = new ZipFile(); @@ -73,7 +74,7 @@ class ZipFileTest extends ZipTestCase { $this->setExpectedException( ZipException::class, - 'Expected Local File Header or (ZIP64) End Of Central Directory Record' + 'Invalid zip file. The end of the central directory could not be found.' ); static::assertNotFalse(file_put_contents($this->outputFilename, random_bytes(255))); @@ -111,7 +112,7 @@ class ZipFileTest extends ZipTestCase { $this->setExpectedException( ZipException::class, - 'Expected Local File Header or (ZIP64) End Of Central Directory Record' + 'Invalid zip file. The end of the central directory could not be found.' ); $zipFile = new ZipFile(); @@ -143,7 +144,7 @@ class ZipFileTest extends ZipTestCase */ public function testOpenFromStreamNullStream() { - $this->setExpectedException(InvalidArgumentException::class, 'Invalid stream resource'); + $this->setExpectedException(InvalidArgumentException::class, 'Stream must be a resource'); $zipFile = new ZipFile(); $zipFile->openFromStream(null); @@ -154,7 +155,7 @@ class ZipFileTest extends ZipTestCase */ public function testOpenFromStreamInvalidResourceType() { - $this->setExpectedException(InvalidArgumentException::class, 'Invalid stream resource'); + $this->setExpectedException(InvalidArgumentException::class, 'Stream must be a resource'); $zipFile = new ZipFile(); /** @noinspection PhpParamsInspection */ @@ -166,7 +167,7 @@ class ZipFileTest extends ZipTestCase */ public function testOpenFromStreamInvalidResourceType2() { - $this->setExpectedException(InvalidArgumentException::class, 'Invalid resource type - gd.'); + $this->setExpectedException(InvalidArgumentException::class, 'Invalid resource type'); $zipFile = new ZipFile(); @@ -184,7 +185,7 @@ class ZipFileTest extends ZipTestCase */ public function testOpenFromStreamInvalidResourceType3() { - $this->setExpectedException(InvalidArgumentException::class, 'Invalid stream type - dir.'); + $this->setExpectedException(InvalidArgumentException::class, 'Directory stream not supported'); $zipFile = new ZipFile(); $zipFile->openFromStream(opendir(__DIR__)); @@ -197,7 +198,7 @@ class ZipFileTest extends ZipTestCase */ public function testOpenFromStreamNoSeekable() { - $this->setExpectedException(InvalidArgumentException::class, 'Resource cannot seekable stream.'); + $this->setExpectedException(InvalidArgumentException::class, 'The stream wrapper type "http" is not supported'); if (!$fp = @fopen('http://localhost', 'rb')) { if (!$fp = @fopen('http://example.org', 'rb')) { @@ -216,7 +217,7 @@ class ZipFileTest extends ZipTestCase */ public function testOpenFromStreamEmptyContents() { - $this->setExpectedException(ZipException::class, 'Invalid zip file'); + $this->setExpectedException(ZipException::class, 'Corrupt zip file'); $fp = fopen($this->outputFilename, 'w+b'); $zipFile = new ZipFile(); @@ -231,7 +232,7 @@ class ZipFileTest extends ZipTestCase { $this->setExpectedException( ZipException::class, - 'Expected Local File Header or (ZIP64) End Of Central Directory Record' + 'Invalid zip file. The end of the central directory could not be found.' ); $fp = fopen($this->outputFilename, 'w+b'); @@ -301,7 +302,7 @@ class ZipFileTest extends ZipTestCase $fileExpected = $this->outputDirname . \DIRECTORY_SEPARATOR . 'file_expected.zip'; $zipFile = new ZipFile(); - $zipFile->addDirRecursive(__DIR__ . '/../../src'); + $zipFile->addDirRecursive(__DIR__ . '/../src'); $sourceCount = $zipFile->count(); static::assertTrue($sourceCount > 0); $zipFile @@ -344,9 +345,9 @@ class ZipFileTest extends ZipTestCase 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 . 'phpunit.xml'); - $outputFromStream = file_get_contents(\dirname(\dirname(__DIR__)) . \DIRECTORY_SEPARATOR . 'composer.json'); + $outputFromString2 = file_get_contents(\dirname(__DIR__) . \DIRECTORY_SEPARATOR . 'README.md'); + $outputFromFile = file_get_contents(\dirname(__DIR__) . \DIRECTORY_SEPARATOR . 'phpunit.xml'); + $outputFromStream = file_get_contents(\dirname(__DIR__) . \DIRECTORY_SEPARATOR . 'composer.json'); $filenameFromString = basename(__FILE__); $filenameFromString2 = 'test_file.txt'; @@ -451,7 +452,7 @@ class ZipFileTest extends ZipTestCase $zipFile->openFile($this->outputFilename); $info = $zipFile->getEntryInfo($basename); - static::assertSame($info->getMethodName(), 'No compression'); + static::assertSame($info->getMethodName(), 'Stored'); $zipFile->close(); } @@ -562,7 +563,7 @@ class ZipFileTest extends ZipTestCase */ public function testDeleteFromName() { - $inputDir = \dirname(\dirname(__DIR__)) . \DIRECTORY_SEPARATOR; + $inputDir = \dirname(__DIR__) . \DIRECTORY_SEPARATOR; $deleteEntryName = 'composer.json'; $zipFile = new ZipFile(); @@ -585,7 +586,7 @@ class ZipFileTest extends ZipTestCase } /** - * @throws Exception\ZipEntryNotFoundException + * @throws ZipEntryNotFoundException * @throws ZipException */ public function testDeleteNewEntry() @@ -615,6 +616,18 @@ class ZipFileTest extends ZipTestCase $zipFile->deleteFromName('entry'); } + public function testCatchNotFoundEntry() + { + $entryName = 'entry'; + $zipFile = new ZipFile(); + + try { + $zipFile->getEntry($entryName); + } catch (ZipEntryNotFoundException $e) { + static::assertSame($e->getEntryName(), $entryName); + } + } + /** * Delete zip entries from glob pattern. * @@ -622,7 +635,7 @@ class ZipFileTest extends ZipTestCase */ public function testDeleteFromGlob() { - $inputDir = \dirname(\dirname(__DIR__)); + $inputDir = \dirname(__DIR__); $zipFile = new ZipFile(); $zipFile->addFilesFromGlobRecursive($inputDir, '**.{xml,json,md}', '/'); @@ -677,7 +690,7 @@ class ZipFileTest extends ZipTestCase */ public function testDeleteFromRegex() { - $inputDir = \dirname(\dirname(__DIR__)); + $inputDir = \dirname(__DIR__); $zipFile = new ZipFile(); $zipFile->addFilesFromRegexRecursive($inputDir, '~\.(xml|json)$~i', 'Path'); @@ -726,7 +739,7 @@ class ZipFileTest extends ZipTestCase public function testDeleteAll() { $zipFile = new ZipFile(); - $zipFile->addDirRecursive(\dirname(\dirname(__DIR__)) . \DIRECTORY_SEPARATOR . 'src'); + $zipFile->addDirRecursive(\dirname(__DIR__) . \DIRECTORY_SEPARATOR . 'src'); static::assertTrue($zipFile->count() > 0); $zipFile->saveAsFile($this->outputFilename); $zipFile->close(); @@ -880,7 +893,7 @@ class ZipFileTest extends ZipTestCase */ public function testVeryLongEntryComment() { - $this->setExpectedException(ZipException::class, 'Comment too long'); + $this->setExpectedException(InvalidArgumentException::class, 'Comment too long'); $comment = 'Very long comment' . \PHP_EOL . 'Очень длинный комментарий' . \PHP_EOL; @@ -913,21 +926,21 @@ class ZipFileTest extends ZipTestCase $entries = [ '1' => [ 'data' => random_bytes(255), - 'method' => ZipFile::METHOD_STORED, - 'expected' => 'No compression', + 'method' => ZipCompressionMethod::STORED, + 'expected' => 'Stored', ], '2' => [ 'data' => random_bytes(255), - 'method' => ZipFile::METHOD_DEFLATED, - 'expected' => 'Deflate', + 'method' => ZipCompressionMethod::DEFLATED, + 'expected' => 'Deflated', ], ]; if (\extension_loaded('bz2')) { $entries['3'] = [ 'data' => random_bytes(255), - 'method' => ZipFile::METHOD_BZIP2, - 'expected' => 'Bzip2', + 'method' => ZipCompressionMethod::BZIP2, + 'expected' => 'BZIP2', ]; } @@ -942,7 +955,7 @@ class ZipFileTest extends ZipTestCase static::assertCorrectZipArchive($this->outputFilename); $zipFile->openFile($this->outputFilename); - $zipFile->setCompressionLevel(ZipFile::LEVEL_BEST_COMPRESSION); + $zipFile->setCompressionLevel(ZipCompressionLevel::MAXIMUM); $zipAllInfo = $zipFile->getAllInfo(); foreach ($zipAllInfo as $entryName => $info) { @@ -954,26 +967,34 @@ class ZipFileTest extends ZipTestCase $zipFile->close(); } - public function testSetInvalidCompressionLevel() + /** + * @dataProvider provideInvalidCompressionLevels + * + * @param int $compressionLevel + */ + public function testSetInvalidCompressionLevel($compressionLevel) { $this->setExpectedException( InvalidArgumentException::class, - 'Invalid compression level. Minimum level -1. Maximum level 9' + 'Invalid compression level. Minimum level 1. Maximum level 9' ); $zipFile = new ZipFile(); - $zipFile->setCompressionLevel(-2); + $zipFile['file 1'] = 'contents'; + $zipFile->setCompressionLevel($compressionLevel); } - public function testSetInvalidCompressionLevel2() + /** + * @return array + */ + public function provideInvalidCompressionLevels() { - $this->setExpectedException( - InvalidArgumentException::class, - 'Invalid compression level. Minimum level -1. Maximum level 9' - ); - - $zipFile = new ZipFile(); - $zipFile->setCompressionLevel(10); + return [ + [-10], + [-2], + [10], + [0xffff], + ]; } /** @@ -1125,10 +1146,7 @@ class ZipFileTest extends ZipTestCase { $this->setExpectedException(ZipException::class, 'Destination is not writable directory'); - /** @noinspection PhpComposerExtensionStubsInspection */ - if (posix_getuid() === 0) { - static::markTestSkipped('Skip the test for a user with root privileges'); - + if (static::skipTestForRootUser()) { return; } @@ -1149,7 +1167,10 @@ class ZipFileTest extends ZipTestCase */ public function testAddFromArrayAccessNullName() { - $this->setExpectedException(InvalidArgumentException::class, 'entryName is null'); + $this->setExpectedException( + InvalidArgumentException::class, + 'Key must not be null, but must contain the name of the zip entry.' + ); $zipFile = new ZipFile(); $zipFile[null] = 'content'; @@ -1160,7 +1181,10 @@ class ZipFileTest extends ZipTestCase */ public function testAddFromArrayAccessEmptyName() { - $this->setExpectedException(InvalidArgumentException::class, 'entryName is empty'); + $this->setExpectedException( + InvalidArgumentException::class, + 'Key is empty, but must contain the name of the zip entry.' + ); $zipFile = new ZipFile(); $zipFile[''] = 'content'; @@ -1193,10 +1217,14 @@ class ZipFileTest extends ZipTestCase */ public function testAddFromStringUnsupportedMethod() { - $this->setExpectedException(ZipUnsupportMethodException::class, 'Unsupported compression method'); + $this->setExpectedException( + ZipUnsupportMethodException::class, + 'Compression method 99 (AES Encryption) is not supported.' + ); $zipFile = new ZipFile(); - $zipFile->addFromString('file', 'contents', ZipEntry::METHOD_WINZIP_AES); + $zipFile->addFromString('file', 'contents', ZipCompressionMethod::WINZIP_AES); + $zipFile->outputAsString(); } /** @@ -1235,8 +1263,8 @@ class ZipFileTest extends ZipTestCase $zipFile->openFile($this->outputFilename); $infoStored = $zipFile->getEntryInfo(basename($fileStored)); $infoDeflated = $zipFile->getEntryInfo(basename($fileDeflated)); - static::assertSame($infoStored->getMethodName(), 'No compression'); - static::assertSame($infoDeflated->getMethodName(), 'Deflate'); + static::assertSame($infoStored->getMethodName(), 'Stored'); + static::assertSame($infoDeflated->getMethodName(), 'Deflated'); $zipFile->close(); } @@ -1270,12 +1298,16 @@ class ZipFileTest extends ZipTestCase */ public function testAddFromStreamUnsupportedMethod() { - $this->setExpectedException(ZipUnsupportMethodException::class, 'Unsupported method'); + $this->setExpectedException( + ZipUnsupportMethodException::class, + 'Compression method 99 (AES Encryption) is not supported.' + ); $handle = fopen(__FILE__, 'rb'); $zipFile = new ZipFile(); - $zipFile->addFromStream($handle, basename(__FILE__), ZipEntry::METHOD_WINZIP_AES); + $zipFile->addFromStream($handle, basename(__FILE__), ZipCompressionMethod::WINZIP_AES); + $zipFile->outputAsString(); } /** @@ -1306,8 +1338,8 @@ class ZipFileTest extends ZipTestCase $zipFile->openFile($this->outputFilename); $infoStored = $zipFile->getEntryInfo(basename($fileStored)); $infoDeflated = $zipFile->getEntryInfo(basename($fileDeflated)); - static::assertSame($infoStored->getMethodName(), 'No compression'); - static::assertSame($infoDeflated->getMethodName(), 'Deflate'); + static::assertSame($infoStored->getMethodName(), 'Stored'); + static::assertSame($infoDeflated->getMethodName(), 'Deflated'); $zipFile->close(); } @@ -1316,7 +1348,7 @@ class ZipFileTest extends ZipTestCase */ public function testAddFileNullFileName() { - $this->setExpectedException(InvalidArgumentException::class, 'file is null'); + $this->setExpectedException(InvalidArgumentException::class, 'Filename is null'); $zipFile = new ZipFile(); $zipFile->addFile(null); @@ -1327,7 +1359,7 @@ class ZipFileTest extends ZipTestCase */ public function testAddFileCantExists() { - $this->setExpectedException(ZipException::class, 'does not exist'); + $this->setExpectedException(InvalidArgumentException::class, 'File path/to/file is not readable'); $zipFile = new ZipFile(); $zipFile->addFile('path/to/file'); @@ -1338,10 +1370,14 @@ class ZipFileTest extends ZipTestCase */ public function testAddFileUnsupportedMethod() { - $this->setExpectedException(ZipUnsupportMethodException::class, 'Unsupported compression method 99'); + $this->setExpectedException( + ZipUnsupportMethodException::class, + 'Compression method 99 (AES Encryption) is not supported.' + ); $zipFile = new ZipFile(); - $zipFile->addFile(__FILE__, null, ZipEntry::METHOD_WINZIP_AES); + $zipFile->addFile(__FILE__, null, ZipCompressionMethod::WINZIP_AES); + $zipFile->outputAsString(); } /** @@ -1349,12 +1385,9 @@ class ZipFileTest extends ZipTestCase */ public function testAddFileCantOpen() { - $this->setExpectedException(ZipException::class, 'file could not be read'); - - /** @noinspection PhpComposerExtensionStubsInspection */ - if (posix_getuid() === 0) { - static::markTestSkipped('Skip the test for a user with root privileges'); + $this->setExpectedException(InvalidArgumentException::class, 'is not readable'); + if (static::skipTestForRootUser()) { return; } @@ -1670,10 +1703,7 @@ class ZipFileTest extends ZipTestCase { $this->setExpectedException(InvalidArgumentException::class, 'can not open from write'); - /** @noinspection PhpComposerExtensionStubsInspection */ - if (posix_getuid() === 0) { - static::markTestSkipped('Skip the test for a user with root privileges'); - + if (static::skipTestForRootUser()) { return; } @@ -1700,17 +1730,21 @@ class ZipFileTest extends ZipTestCase $files['file' . $i . '.txt'] = random_bytes(255); } - $methods = [ZipFile::METHOD_STORED, ZipFile::METHOD_DEFLATED]; + $compressionMethods = [ZipCompressionMethod::STORED, ZipCompressionMethod::DEFLATED]; if (\extension_loaded('bz2')) { - $methods[] = ZipFile::METHOD_BZIP2; + $compressionMethods[] = ZipCompressionMethod::BZIP2; } $zipFile = new ZipFile(); - $zipFile->setCompressionLevel(ZipFile::LEVEL_BEST_SPEED); + $zipFile->setCompressionLevel(ZipCompressionLevel::SUPER_FAST); + + $i = 0; + $countMethods = \count($compressionMethods); foreach ($files as $entryName => $content) { - $zipFile->addFromString($entryName, $content, $methods[array_rand($methods)]); + $compressionMethod = $compressionMethods[$i++ % $countMethods]; + $zipFile->addFromString($entryName, $content, $compressionMethod); } $zipFile->saveAsFile($this->outputFilename); $zipFile->close(); @@ -1804,7 +1838,7 @@ class ZipFileTest extends ZipTestCase } /** - * @throws Exception\ZipEntryNotFoundException + * @throws ZipEntryNotFoundException * @throws ZipException * @throws \Exception */ @@ -1815,16 +1849,15 @@ class ZipFileTest extends ZipTestCase $zipFile->addFromString('file', 'content', ZipEntry::UNKNOWN); $zipFile->addFromString('file2', base64_encode(random_bytes(512)), ZipEntry::UNKNOWN); - static::assertSame($zipFile->getEntryInfo('file')->getMethodName(), 'Unknown'); - static::assertSame($zipFile->getEntryInfo('file2')->getMethodName(), 'Unknown'); + static::assertSame($zipFile->getEntryInfo('file')->getMethodName(), 'Stored'); + static::assertSame($zipFile->getEntryInfo('file2')->getMethodName(), 'Deflated'); $zipFile->saveAsFile($this->outputFilename); $zipFile->close(); $zipFile->openFile($this->outputFilename); - - static::assertSame($zipFile->getEntryInfo('file')->getMethodName(), 'No compression'); - static::assertSame($zipFile->getEntryInfo('file2')->getMethodName(), 'Deflate'); + static::assertSame($zipFile->getEntryInfo('file')->getMethodName(), 'Stored'); + static::assertSame($zipFile->getEntryInfo('file2')->getMethodName(), 'Deflated'); $zipFile->close(); } @@ -1986,21 +2019,23 @@ class ZipFileTest extends ZipTestCase } /** + * @dataProvider provideCompressionLevels + * + * @param int $compressionLevel + * * @throws ZipEntryNotFoundException * @throws ZipException + * @throws \Exception */ - public function testCompressionLevel() + public function testCompressionLevel($compressionLevel) { + $fileContent = random_bytes(512); + $entryName = 'file.txt'; + $zipFile = new ZipFile(); $zipFile - ->addFromString('file', 'content', ZipFile::METHOD_DEFLATED) - ->setCompressionLevelEntry('file', ZipFile::LEVEL_BEST_COMPRESSION) - ->addFromString('file2', 'content', ZipFile::METHOD_DEFLATED) - ->setCompressionLevelEntry('file2', ZipFile::LEVEL_FAST) - ->addFromString('file3', 'content', ZipFile::METHOD_DEFLATED) - ->setCompressionLevelEntry('file3', ZipFile::LEVEL_SUPER_FAST) - ->addFromString('file4', 'content', ZipFile::METHOD_DEFLATED) - ->setCompressionLevelEntry('file4', ZipFile::LEVEL_DEFAULT_COMPRESSION) + ->addFromString($entryName, $fileContent, ZipCompressionMethod::DEFLATED) + ->setCompressionLevelEntry($entryName, $compressionLevel) ->saveAsFile($this->outputFilename) ->close() ; @@ -2008,29 +2043,25 @@ class ZipFileTest extends ZipTestCase static::assertCorrectZipArchive($this->outputFilename); $zipFile->openFile($this->outputFilename); - static::assertSame( - $zipFile->getEntryInfo('file') - ->getCompressionLevel(), - ZipFile::LEVEL_BEST_COMPRESSION - ); - static::assertSame( - $zipFile->getEntryInfo('file2') - ->getCompressionLevel(), - ZipFile::LEVEL_FAST - ); - static::assertSame( - $zipFile->getEntryInfo('file3') - ->getCompressionLevel(), - ZipFile::LEVEL_SUPER_FAST - ); - static::assertSame( - $zipFile->getEntryInfo('file4') - ->getCompressionLevel(), - ZipFile::LEVEL_DEFAULT_COMPRESSION - ); + static::assertSame($zipFile->getEntryContents($entryName), $fileContent); + static::assertSame($zipFile->getEntry($entryName)->getCompressionLevel(), $compressionLevel); + static::assertSame($zipFile->getEntryInfo($entryName)->getCompressionLevel(), $compressionLevel); $zipFile->close(); } + /** + * @return array + */ + public function provideCompressionLevels() + { + return [ + [ZipCompressionLevel::MAXIMUM], + [ZipCompressionLevel::NORMAL], + [ZipCompressionLevel::FAST], + [ZipCompressionLevel::SUPER_FAST], + ]; + } + /** * @throws ZipException */ @@ -2062,10 +2093,10 @@ class ZipFileTest extends ZipTestCase { $zipFile = new ZipFile(); for ($i = 0; $i < 10; $i++) { - $zipFile->addFromString('file' . $i, 'content', ZipFile::METHOD_DEFLATED); + $zipFile->addFromString('file' . $i, 'content', ZipCompressionMethod::DEFLATED); } $zipFile - ->setCompressionLevel(ZipFile::LEVEL_BEST_SPEED) + ->setCompressionLevel(ZipCompressionLevel::SUPER_FAST) ->saveAsFile($this->outputFilename) ->close() ; @@ -2077,7 +2108,7 @@ class ZipFileTest extends ZipTestCase array_walk( $infoList, function (ZipInfo $zipInfo) { - $this->assertSame($zipInfo->getCompressionLevel(), ZipFile::LEVEL_BEST_SPEED); + $this->assertSame($zipInfo->getCompressionLevel(), ZipCompressionLevel::SUPER_FAST); } ); $zipFile->close(); @@ -2090,17 +2121,17 @@ class ZipFileTest extends ZipTestCase public function testCompressionMethodEntry() { $zipFile = new ZipFile(); - $zipFile->addFromString('file', 'content', ZipFile::METHOD_STORED); + $zipFile->addFromString('file', 'content', ZipCompressionMethod::STORED); $zipFile->saveAsFile($this->outputFilename); $zipFile->close(); $zipFile->openFile($this->outputFilename); - static::assertSame($zipFile->getEntryInfo('file')->getMethodName(), 'No compression'); - $zipFile->setCompressionMethodEntry('file', ZipFile::METHOD_DEFLATED); - static::assertSame($zipFile->getEntryInfo('file')->getMethodName(), 'Deflate'); + static::assertSame($zipFile->getEntryInfo('file')->getMethodName(), 'Stored'); + $zipFile->setCompressionMethodEntry('file', ZipCompressionMethod::DEFLATED); + static::assertSame($zipFile->getEntryInfo('file')->getMethodName(), 'Deflated'); $zipFile->rewrite(); - static::assertSame($zipFile->getEntryInfo('file')->getMethodName(), 'Deflate'); + static::assertSame($zipFile->getEntryInfo('file')->getMethodName(), 'Deflated'); } /** @@ -2108,11 +2139,15 @@ class ZipFileTest extends ZipTestCase */ public function testInvalidCompressionMethodEntry() { - $this->setExpectedException(ZipUnsupportMethodException::class, 'Unsupported method'); + $this->setExpectedException( + ZipUnsupportMethodException::class, + 'Compression method 99 (AES Encryption) is not supported.' + ); $zipFile = new ZipFile(); - $zipFile->addFromString('file', 'content', ZipFile::METHOD_STORED); + $zipFile->addFromString('file', 'content', ZipCompressionMethod::STORED); $zipFile->setCompressionMethodEntry('file', 99); + $zipFile->outputAsString(); } /** @@ -2268,4 +2303,90 @@ class ZipFileTest extends ZipTestCase ['file.apk', null, 'application/vnd.android.package-archive', true, 'attachment'], ]; } + + /** + * @dataProvider provideGetEntryStream + * + * @param ZipFile $zipFile + * @param string $entryName + * @param string $contents + * + * @throws ZipEntryNotFoundException + * @throws ZipException + */ + public function testReopenEntryStream(ZipFile $zipFile, $entryName, $contents) + { + for ($i = 0; $i < 2; $i++) { + $fp = $zipFile->getEntryStream($entryName); + static::assertInternalType('resource', $fp); + static::assertSame(stream_get_contents($fp), $contents); + fclose($fp); + } + + $zipFile->close(); + } + + /** + * @throws \Exception + * + * @return array + */ + public function provideGetEntryStream() + { + $entryName = 'entry'; + $contents = random_bytes(1024); + + $zipFileSpl = new ZipFile(); + $zipFileSpl->addSplFile(new \SplFileInfo(__FILE__), $entryName); + + return [ + [(new ZipFile())->addFromString($entryName, $contents), $entryName, $contents], + [(new ZipFile())->addFile(__FILE__, $entryName), $entryName, file_get_contents(__FILE__)], + [ + (new ZipFile())->addFromStream(fopen(__FILE__, 'rb'), $entryName), + $entryName, + file_get_contents(__FILE__), + ], + [$zipFileSpl, $entryName, file_get_contents(__FILE__)], + ]; + } + + /** + * @throws ZipException + */ + public function testGetEntries() + { + $zipFile = new ZipFile(); + for ($i = 0; $i < 100; $i++) { + $zipFile->addFromString($i . '.txt', 'contents ' . $i); + } + $zipFile->saveAsFile($this->outputFilename); + $zipFile->close(); + + $zipFile->openFile($this->outputFilename); + $zipEntries = $zipFile->getEntries(); + static::assertCount(100, $zipEntries); + + foreach ($zipEntries as $zipEntry) { + static::assertInstanceOf(ZipEntry::class, $zipEntry); + static::assertNotSame($zipEntry->getDosTime(), 0); + $zipEntry->setDosTime(0); + $zipEntry->setCreatedOS(ZipPlatform::OS_DOS); + $zipEntry->setExtractedOS(ZipPlatform::OS_DOS); + $zipEntry->setInternalAttributes(1); + $zipEntry->setExternalAttributes(0); + } + $zipFile->rewrite(); + + self::assertCorrectZipArchive($this->outputFilename); + + foreach ($zipFile->getEntries() as $zipEntry) { + static::assertSame($zipEntry->getDosTime(), 0); + static::assertSame($zipEntry->getExtractedOS(), ZipPlatform::OS_DOS); + static::assertSame($zipEntry->getCreatedOS(), ZipPlatform::OS_DOS); + static::assertSame($zipEntry->getInternalAttributes(), 1); + static::assertSame($zipEntry->getExternalAttributes(), 0); + } + $zipFile->close(); + } } diff --git a/tests/ZipFinderTest.php b/tests/ZipFinderTest.php new file mode 100644 index 0000000..67cdbcd --- /dev/null +++ b/tests/ZipFinderTest.php @@ -0,0 +1,42 @@ +files() + ->name('*.php') + ->in(__DIR__) + ; + $zipFile = new ZipFile(); + $zipFile->addFromFinder( + $finder, + [ + ZipOptions::COMPRESSION_METHOD => ZipCompressionMethod::DEFLATED, + ] + ); + $zipFile->saveAsFile($this->outputFilename); + + static::assertCorrectZipArchive($this->outputFilename); + + static::assertSame($finder->count(), $zipFile->count()); + $zipFile->close(); + } +} diff --git a/tests/PhpZip/ZipMatcherTest.php b/tests/ZipMatcherTest.php similarity index 98% rename from tests/PhpZip/ZipMatcherTest.php rename to tests/ZipMatcherTest.php index 345fa3d..415af67 100644 --- a/tests/PhpZip/ZipMatcherTest.php +++ b/tests/ZipMatcherTest.php @@ -1,10 +1,11 @@ addDir(__DIR__); - $zipFile->setPassword($password, ZipFile::ENCRYPTION_METHOD_TRADITIONAL); + $zipFile->setPassword($password, ZipEncryptionMethod::PKWARE); $zipFile->saveAsFile($this->outputFilename); $zipFile->close(); static::assertCorrectZipArchive($this->outputFilename, $password); - // check bad password for ZipCrypto $zipFile->openFile($this->outputFilename); + // check bad password for Traditional PKWARE encryption $zipFile->setReadPassword($badPassword); foreach ($zipFile->getListFiles() as $entryName) { @@ -54,23 +58,23 @@ class ZipPasswordTest extends ZipFileAddDirTest $zipFile[$entryName]; static::fail('Expected Exception has not been raised.'); } catch (ZipAuthenticationException $ae) { - static::assertContains('Invalid password for zip entry', $ae->getMessage()); + static::assertContains('Invalid password', $ae->getMessage()); } } - // check correct password for ZipCrypto + // check correct password for Traditional PKWARE encryption $zipFile->setReadPassword($password); foreach ($zipFile->getAllInfo() as $info) { static::assertTrue($info->isEncrypted()); - static::assertContains('ZipCrypto', $info->getMethodName()); + static::assertContains('Traditional PKWARE encryption', $info->getEncryptionMethodName()); $decryptContent = $zipFile[$info->getName()]; static::assertNotEmpty($decryptContent); static::assertContains('setPassword($password, ZipFile::ENCRYPTION_METHOD_WINZIP_AES); + $zipFile->setPassword($password, ZipEncryptionMethod::WINZIP_AES_256); $zipFile->saveAsFile($this->outputFilename); $zipFile->close(); @@ -78,6 +82,7 @@ class ZipPasswordTest extends ZipFileAddDirTest // check from WinZip AES encryption $zipFile->openFile($this->outputFilename); + // set bad password WinZip AES $zipFile->setReadPassword($badPassword); @@ -95,7 +100,8 @@ class ZipPasswordTest extends ZipFileAddDirTest foreach ($zipFile->getAllInfo() as $info) { static::assertTrue($info->isEncrypted()); - static::assertContains('WinZip', $info->getMethodName()); + static::assertContains('Deflated', $info->getMethodName()); + static::assertContains('WinZip AES-256', $info->getEncryptionMethodName()); $decryptContent = $zipFile[$info->getName()]; static::assertNotEmpty($decryptContent); static::assertContains('addDirRecursive($this->outputDirname); - $zip->setPassword($password, ZipFile::ENCRYPTION_METHOD_TRADITIONAL); + $zip->setPassword($password, ZipEncryptionMethod::PKWARE); $zip->saveAsFile($this->outputFilename); $zip->close(); @@ -149,7 +155,7 @@ class ZipPasswordTest extends ZipFileAddDirTest foreach ($zip->getAllInfo() as $info) { if (!$info->isFolder()) { static::assertTrue($info->isEncrypted()); - static::assertContains('ZipCrypto', $info->getMethodName()); + static::assertContains('Traditional PKWARE encryption', $info->getEncryptionMethodName()); } } $zip->close(); @@ -184,7 +190,7 @@ class ZipPasswordTest extends ZipFileAddDirTest if (!$info->isFolder()) { static::assertTrue($info->isEncrypted()); static::assertSame($info->getEncryptionMethod(), $encryptionMethod); - static::assertContains('WinZip AES-' . $bitSize, $info->getMethodName()); + static::assertContains('WinZip AES-' . $bitSize, $info->getEncryptionMethodName()); } } $zip->close(); @@ -196,15 +202,14 @@ class ZipPasswordTest extends ZipFileAddDirTest public function winZipKeyStrengthProvider() { return [ - [ZipFile::ENCRYPTION_METHOD_WINZIP_AES_128, 128], - [ZipFile::ENCRYPTION_METHOD_WINZIP_AES_192, 192], - [ZipFile::ENCRYPTION_METHOD_WINZIP_AES, 256], - [ZipFile::ENCRYPTION_METHOD_WINZIP_AES_256, 256], + [ZipEncryptionMethod::WINZIP_AES_128, 128], + [ZipEncryptionMethod::WINZIP_AES_192, 192], + [ZipEncryptionMethod::WINZIP_AES_256, 256], ]; } /** - * @throws Exception\ZipEntryNotFoundException + * @throws ZipEntryNotFoundException * @throws ZipException */ public function testEncryptionEntries() @@ -221,8 +226,8 @@ class ZipPasswordTest extends ZipFileAddDirTest $zip = new ZipFile(); $zip->addDir($this->outputDirname); - $zip->setPasswordEntry('.hidden', $password1, ZipFile::ENCRYPTION_METHOD_TRADITIONAL); - $zip->setPasswordEntry('text file.txt', $password2, ZipFile::ENCRYPTION_METHOD_WINZIP_AES); + $zip->setPasswordEntry('.hidden', $password1, ZipEncryptionMethod::PKWARE); + $zip->setPasswordEntry('text file.txt', $password2, ZipEncryptionMethod::WINZIP_AES_256); $zip->saveAsFile($this->outputFilename); $zip->close(); @@ -236,16 +241,17 @@ class ZipPasswordTest extends ZipFileAddDirTest 'text file.txt', 'Текстовый документ.txt', 'empty dir/', + 'LoremIpsum.txt', ] ); $info = $zip->getEntryInfo('.hidden'); static::assertTrue($info->isEncrypted()); - static::assertContains('ZipCrypto', $info->getMethodName()); + static::assertContains('Traditional PKWARE encryption', $info->getEncryptionMethodName()); $info = $zip->getEntryInfo('text file.txt'); static::assertTrue($info->isEncrypted()); - static::assertContains('WinZip AES', $info->getMethodName()); + static::assertContains('WinZip AES', $info->getEncryptionMethodName()); static::assertFalse($zip->getEntryInfo('Текстовый документ.txt')->isEncrypted()); static::assertFalse($zip->getEntryInfo('empty dir/')->isEncrypted()); @@ -254,7 +260,7 @@ class ZipPasswordTest extends ZipFileAddDirTest } /** - * @throws Exception\ZipEntryNotFoundException + * @throws ZipEntryNotFoundException * @throws ZipException */ public function testEncryptionEntriesWithDefaultPassword() @@ -273,8 +279,8 @@ class ZipPasswordTest extends ZipFileAddDirTest $zip = new ZipFile(); $zip->addDir($this->outputDirname); $zip->setPassword($defaultPassword); - $zip->setPasswordEntry('.hidden', $password1, ZipFile::ENCRYPTION_METHOD_TRADITIONAL); - $zip->setPasswordEntry('text file.txt', $password2, ZipFile::ENCRYPTION_METHOD_WINZIP_AES); + $zip->setPasswordEntry('.hidden', $password1, ZipEncryptionMethod::PKWARE); + $zip->setPasswordEntry('text file.txt', $password2, ZipEncryptionMethod::WINZIP_AES_256); $zip->saveAsFile($this->outputFilename); $zip->close(); @@ -289,20 +295,21 @@ class ZipPasswordTest extends ZipFileAddDirTest 'text file.txt', 'Текстовый документ.txt', 'empty dir/', + 'LoremIpsum.txt', ] ); $info = $zip->getEntryInfo('.hidden'); static::assertTrue($info->isEncrypted()); - static::assertContains('ZipCrypto', $info->getMethodName()); + static::assertContains('Traditional PKWARE encryption', $info->getEncryptionMethodName()); $info = $zip->getEntryInfo('text file.txt'); static::assertTrue($info->isEncrypted()); - static::assertContains('WinZip AES', $info->getMethodName()); + static::assertContains('WinZip AES', $info->getEncryptionMethodName()); $info = $zip->getEntryInfo('Текстовый документ.txt'); static::assertTrue($info->isEncrypted()); - static::assertContains('WinZip AES', $info->getMethodName()); + static::assertContains('WinZip AES', $info->getEncryptionMethodName()); static::assertFalse($zip->getEntryInfo('empty dir/')->isEncrypted()); @@ -314,17 +321,17 @@ class ZipPasswordTest extends ZipFileAddDirTest */ public function testSetEncryptionMethodInvalid() { - $this->setExpectedException(ZipException::class, 'Invalid encryption method'); + $this->setExpectedException(InvalidArgumentException::class, 'Encryption method 9999 is not supported.'); $zipFile = new ZipFile(); $encryptionMethod = 9999; - $zipFile->setPassword('pass', $encryptionMethod); $zipFile['entry'] = 'content'; + $zipFile->setPassword('pass', $encryptionMethod); $zipFile->outputAsString(); } /** - * @throws Exception\ZipEntryNotFoundException + * @throws ZipEntryNotFoundException * @throws ZipException */ public function testEntryPassword() @@ -362,11 +369,11 @@ class ZipPasswordTest extends ZipFileAddDirTest */ public function testInvalidEncryptionMethodEntry() { - $this->setExpectedException(ZipException::class, 'Invalid encryption method'); + $this->setExpectedException(InvalidArgumentException::class, 'Encryption method 99 is not supported.'); $zipFile = new ZipFile(); - $zipFile->addFromString('file', 'content', ZipFile::METHOD_STORED); - $zipFile->setPasswordEntry('file', 'pass', 99); + $zipFile->addFromString('file', 'content', ZipCompressionMethod::STORED); + $zipFile->setPasswordEntry('file', 'pass', ZipCompressionMethod::WINZIP_AES); } /** @@ -424,11 +431,10 @@ class ZipPasswordTest extends ZipFileAddDirTest $contents = str_pad('', 1000, 'test;test2;test3' . \PHP_EOL, \STR_PAD_RIGHT); $password = base64_encode(random_bytes(20)); - $encryptMethod = ZipFile::ENCRYPTION_METHOD_WINZIP_AES_256; $zipFile = new ZipFile(); $zipFile - ->addFromString('codes.csv', $contents) - ->setPassword($password, $encryptMethod) + ->addFromString('codes.csv', $contents, ZipCompressionMethod::DEFLATED) + ->setPassword($password, ZipEncryptionMethod::WINZIP_AES_256) ->saveAsFile($this->outputFilename) ->close() ; @@ -453,6 +459,7 @@ class ZipPasswordTest extends ZipFileAddDirTest $zipFile = new ZipFile(); $zipFile->openFile($file); $zipFile->setReadPassword($password); + $zipFile->setPassword($password); $zipFile->setEntryComment('contents.txt', 'comment'); // change entry, but not changed contents $zipFile->saveAsFile($this->outputFilename); diff --git a/tests/PhpZip/ZipRemoteFileTest.php b/tests/ZipRemoteFileTest.php similarity index 97% rename from tests/PhpZip/ZipRemoteFileTest.php rename to tests/ZipRemoteFileTest.php index 5047d04..bfe425e 100644 --- a/tests/PhpZip/ZipRemoteFileTest.php +++ b/tests/ZipRemoteFileTest.php @@ -1,8 +1,9 @@ addFromStream($fp, $fileName); - $zipFile->saveAsFile($outputZip); $zipFile->close(); diff --git a/tests/PhpZip/ZipSlipVulnerabilityTest.php b/tests/ZipSlipVulnerabilityTest.php similarity index 88% rename from tests/PhpZip/ZipSlipVulnerabilityTest.php rename to tests/ZipSlipVulnerabilityTest.php index 45d92e7..a283cbd 100644 --- a/tests/PhpZip/ZipSlipVulnerabilityTest.php +++ b/tests/ZipSlipVulnerabilityTest.php @@ -1,6 +1,9 @@ setExpectedException( + $exceptionClass, + $exceptionMessage + ); + } + + static::assertInternalType('resource', $resource); + + $zipFile = new ZipFile(); + $zipFile->openFromStream($resource); + + static::assertTrue(fclose($resource)); + } + + /** + * @return array|\Generator + */ + public function provideStreams() + { + return [ + 'file' => yield [fopen(__DIR__ . '/resources/apk.zip', 'rb'), null, null], + 'directory' => yield [ + fopen(__DIR__, 'rb'), + InvalidArgumentException::class, + 'Directory stream not supported', + ], + 'temp' => yield [$this->getTempResource('php://temp'), null, null], + 'memory' => yield [$this->getTempResource('php://memory'), null, null], + 'bz' => yield [ + $this->getBzResource(), + InvalidArgumentException::class, + 'The stream wrapper type "Unknown" is not supported.', + ], + 'url' => yield [ + fopen('https://github.com/Ne-Lexa/php-zip/archive/master.zip', 'rb'), + InvalidArgumentException::class, + 'The stream wrapper type "http" is not supported.', + ], + 'ftp' => yield [ + fopen('ftp://ftp.ripe.net/pub/stats/ripencc/delegated-ripencc-latest.md5', 'rb'), + InvalidArgumentException::class, + 'The stream wrapper type "ftp" is not supported.', + ], + ]; + } + + /** + * @param string $filename + * + * @return resource + */ + private function getTempResource($filename) + { + $fp = fopen(__DIR__ . '/resources/apk.zip', 'rb'); + $stream = fopen($filename, 'r+b'); + stream_copy_to_stream($fp, $stream); + fclose($fp); + rewind($stream); + + return $stream; + } + + /** + * @return resource|null + */ + private function getBzResource() + { + if (!\extension_loaded('bz2')) { + return null; + } + $stream = bzopen('php://temp', 'w'); + bzwrite($stream, 'some input here'); + + return $stream; + } +} diff --git a/tests/PhpZip/ZipTestCase.php b/tests/ZipTestCase.php similarity index 89% rename from tests/PhpZip/ZipTestCase.php rename to tests/ZipTestCase.php index ab61665..d900208 100644 --- a/tests/PhpZip/ZipTestCase.php +++ b/tests/ZipTestCase.php @@ -1,9 +1,9 @@