diff --git a/README.md b/README.md index 76526e2..68bd70e 100644 --- a/README.md +++ b/README.md @@ -1,250 +1,455 @@ -## Documentation +`PhpZip` Version 2 +================ +`PhpZip` - is to create, update, opening and unpacking ZIP archives in pure PHP. -Create and manipulate zip archives. No use ZipArchive class and php-zip extension. +The library supports `ZIP64`, `Traditional PKWARE Encryption` and `WinZIP AES Encryption`. -### class \Nelexa\Zip\ZipFile -Initialization +The library does not require extension `php-xml` and class `ZipArchive`. + +Requirements +------------ +- `PHP` >= 5.4 (64 bit) +- Php-extension `mbstring` +- Optional php-extension `bzip2` for BZIP2 compression. +- Optional php-extension `openssl` or `mcrypt` for `WinZip Aes Encryption` support. + +Installation +------------ +`composer require nelexa/zip` + +Documentation +------------- +#### Class `\PhpZip\ZipFile` (open, extract, info) +Open zip archive from file. ```php -$zip = new \Nelexa\Zip\ZipFile(); +$zipFile = \PhpZip\ZipFile::openFromFile($filename); ``` -Create archive +Open zip archive from data string. ```php -$zip->create(); +$data = file_get_contents($filename); +$zipFile = \PhpZip\ZipFile::openFromString($data); ``` -Open archive file +Open zip archive from stream resource. ```php -$zip->open($filename); +$stream = fopen($filename, 'rb'); +$zipFile = \PhpZip\ZipFile::openFromStream($stream); ``` -Open archive from string +Get num entries. ```php -$zip->openFromString($string) +$count = $zipFile->count(); +// or +$count = count($zipFile); ``` -Set password +Get list files. ```php -$zip->setPassword($password); +$listFiles = $zipFile->getListFiles(); ``` -List files +Foreach zip entries. ```php -$listFiles = $zip->getListFiles(); +foreach($zipFile as $entryName => $dataContent){ + echo "Entry: $entryName" . PHP_EOL; + echo "Data: $dataContent" . PHP_EOL; + echo "-----------------------------" . PHP_EOL; +} ``` -Get count files +Iterator zip entries. ```php -$countFiles = $zip->getCountFiles(); +$iterator = new \ArrayIterator($zipFile); +while ($iterator->valid()) +{ + $entryName = $iterator->key(); + $dataContent = $iterator->current(); + + echo "Entry: $entryName" . PHP_EOL; + echo "Data: $dataContent" . PHP_EOL; + echo "-----------------------------" . PHP_EOL; + + $iterator->next(); +} +``` +Checks whether a entry exists. +```php +$boolValue = $zipFile->hasEntry($entryName); +``` +Check whether the directory entry. +```php +$boolValue = $zipFile->isDirectory($entryName); +``` +Set password to all encrypted entries. +```php +$zipFile->setPassword($password); +``` +Set password to concrete zip entry. +```php +$zipFile->setEntryPassword($entryName, $password); +``` +Get comment archive. +```php +$commentArchive = $zipFile->getComment(); +``` +Get comment zip entry. +```php +$commentEntry = $zipFile->getEntryComment($entryName); +``` +Get entry info. +```php +$zipInfo = $zipFile->getEntryInfo('file.txt'); +echo $zipInfo . PHP_EOL; +// ZipInfo {Path="file.txt", Size=9.77KB, Compressed size=2.04KB, Modified time=2016-09-24T19:25:10+03:00, Crc=0x4b5ab5c7, Method="Deflate", Platform="UNIX", Version=20} +print_r($zipInfo); +//PhpZip\Model\ZipInfo Object +//( +// [path:PhpZip\Model\ZipInfo:private] => file.txt +// [folder:PhpZip\Model\ZipInfo:private] => +// [size:PhpZip\Model\ZipInfo:private] => 10000 +// [compressedSize:PhpZip\Model\ZipInfo:private] => 2086 +// [mtime:PhpZip\Model\ZipInfo:private] => 1474734310 +// [ctime:PhpZip\Model\ZipInfo:private] => +// [atime:PhpZip\Model\ZipInfo:private] => +// [encrypted:PhpZip\Model\ZipInfo:private] => +// [comment:PhpZip\Model\ZipInfo:private] => +// [crc:PhpZip\Model\ZipInfo:private] => 1264235975 +// [method:PhpZip\Model\ZipInfo:private] => Deflate +// [platform:PhpZip\Model\ZipInfo:private] => UNIX +// [version:PhpZip\Model\ZipInfo:private] => 20 +//) +``` +Get info for all entries. +```php +$zipAllInfo = $zipFile->getAllInfo(); +print_r($zipAllInfo); +//Array +//( +// [file.txt] => PhpZip\Model\ZipInfo Object +// ( +// ... +// ) +// +// [file2.txt] => PhpZip\Model\ZipInfo Object +// ( +// ... +// ) +// +// ... +//) +``` +Extract all files to directory. +```php +$zipFile->extractTo($directory); +``` +Extract some files to directory. +```php +$extractOnlyFiles = ["filename1", "filename2", "dir/dir/dir/"]; +$zipFile->extractTo($directory, $extractOnlyFiles); +``` +Get entry content. +```php +$data = $zipFile->getEntryContent($entryName); +``` +Close zip archive. +```php +$zipFile->close(); +``` +#### Class `\PhpZip\ZipOutputFile` (create, update, extract) +Create zip archive. +```php +$zipOutputFile = new \PhpZip\ZipOutputFile(); +// or +$zipOutputFile = \PhpZip\ZipOutputFile::create(); +``` +Open zip file from update. +```php +// initial ZipFile +$zipFile = \PhpZip\ZipFile::openFromFile($filename); + +// Create output stream from update zip file +$zipOutputFile = new \PhpZip\ZipOutputFile($zipFile); +// or +$zipOutputFile = \PhpZip\ZipOutputFile::openFromZipFile($zipFile); +``` +Add entry from file. +```php +$zipOutputFile->addFromFile($filename); // $entryName == basename($filename); +$zipOutputFile->addFromFile($filename, $entryName); +$zipOutputFile->addFromFile($filename, $entryName, ZipEntry::METHOD_DEFLATED); +$zipOutputFile->addFromFile($filename, null, ZipEntry::METHOD_BZIP2); // $entryName == basename($filename); +``` +Add entry from string data. +```php +$zipOutputFile->addFromString($entryName, $data) +$zipOutputFile->addFromString($entryName, $data, ZipEntry::METHOD_DEFLATED) +``` +Add entry from stream. +```php +$zipOutputFile->addFromStream($stream, $entryName) +$zipOutputFile->addFromStream($stream, $entryName, ZipEntry::METHOD_DEFLATED) ``` Add empty dir ```php -$zip->addEmptyDir($dirName); +$zipOutputFile->addEmptyDir($dirName); ``` -Add dir +Add a directory **recursively** to the archive. ```php -$directory = "/tmp"; -$ignoreFiles = array("xxx.file", "xxx2.file"); -$zip->addDir($directory); // add path /tmp to / -$zip->addDir($directory, "var/temp"); // add path /tmp to var/temp -$zip->addDir($directory, "var/temp", $ignoreFiles); // add path /tmp to var/temp and ignore files xxx.file and xxx2.file +$zipOutputFile->addDir($dirName); +// or +$zipOutputFile->addDir($dirName, true); ``` -Add files from glob pattern +Add a directory **not recursively** to the archive. ```php -$zip->addGlob("music/*.mp3"); // add all mp3 files +$zipOutputFile->addDir($dirName, false); ``` -Add files from regex pattern +Add a directory to the archive by path `$moveToPath` ```php -$zip->addPattern("~file[0-9]+\.jpg$~", "picture/"); +$moveToPath = 'dir/subdir/'; +$zipOutputFile->addDir($dirName, $boolResursive, $moveToPath); ``` -Add file +Add a directory to the archive with ignoring files. ```php -$zip->addFile($filename); -$zip->addFile($filename, $localName); -$zip->addFile($filename, $localName, \Nelexa\Zip\ZipEntry::COMPRESS_METHOD_STORED); // no compression -$zip->addFile($filename, $localName, \Nelexa\Zip\ZipEntry::COMPRESS_METHOD_DEFLATED); +$ignoreFiles = ["file_ignore.txt", "dir_ignore/sub dir ignore/"]; +$zipOutputFile->addDir($dirName, $boolResursive, $moveToPath, $ignoreFiles); ``` -Add file from string +Add a directory and set compression method. ```php -$zip->addFromString($localName, $contents); -$zip->addFromString($localName, $contents, \Nelexa\Zip\ZipEntry::COMPRESS_METHOD_STORED); // no compression -$zip->addFromString($localName, $contents, \Nelexa\Zip\ZipEntry::COMPRESS_METHOD_DEFLATED); +$compressionMethod = ZipEntry::METHOD_DEFLATED; +$zipOutputFile->addDir($dirName, $boolRecursive, $moveToPath, $ignoreFiles, $compressionMethod); ``` -Update timestamp for all files +Add a files **recursively** from [glob pattern](https://en.wikipedia.org/wiki/Glob_(programming)) to the archive. ```php -$timestamp = time(); // now time -$zip->updateTimestamp($timestamp); +$globPattern = '**.{jpg,jpeg,png,gif}'; // example glob pattern -> add all .jpg, .jpeg, .png and .gif files +$zipOutputFile->addFilesFromGlob($inputDir, $globPattern); ``` -Delete files from glob pattern +Add a files **not recursively** from [glob pattern](https://en.wikipedia.org/wiki/Glob_(programming)) to the archive. ```php -$zip->deleteGlob("*.jpg"); // remove all jpg files +$recursive = false; +$zipOutputFile->addFilesFromGlob($inputDir, $globPattern, $recursive); ``` -Delete files from regex pattern +Add a files from [glob pattern](https://en.wikipedia.org/wiki/Glob_(programming)) to the archive by path `$moveToPath`. ```php -$zip->deletePattern("~\.jpg$~i"); // remove all jpg files +$moveToPath = 'dir/dir2/dir3'; +$zipOutputFile->addFilesFromGlob($inputDir, $globPattern, $recursive = true, $moveToPath); ``` -Delete file from index +Add a files from [glob pattern](https://en.wikipedia.org/wiki/Glob_(programming)) to the archive and set compression method. ```php -$zip->deleteIndex(0); +$compressionMethod = ZipEntry::METHOD_DEFLATED; +$zipOutputFile->addFilesFromGlob($inputDir, $globPattern, $recursive, $moveToPath, $compressionMethod); ``` -Delete all files +Add a files **recursively** from [RegEx (Regular Expression) pattern](https://en.wikipedia.org/wiki/Regular_expression) to the archive. ```php -$zip->deleteAll(); +$regexPattern = '/\.(jpe?g|png|gif)$/si'; // example regex pattern -> add all .jpg, .jpeg, .png and .gif files +$zipOutputFile->addFilesFromRegex($inputDir, $regexPattern); ``` -Delete from file name +Add a files **not recursively** from [RegEx (Regular Expression) pattern](https://en.wikipedia.org/wiki/Regular_expression) to the archive. ```php -$zip->deleteName($filename); +$recursive = false; +$zipOutputFile->addFilesFromRegex($inputDir, $regexPattern, $recursive); ``` -Extract zip archive +Add a files from [RegEx (Regular Expression) pattern](https://en.wikipedia.org/wiki/Regular_expression) to the archive by path `$moveToPath`. ```php -$zip->extractTo($toPath) -$zip->extractTo($toPath, array("file1", "file2")); // extract only files file1 and file2 +$moveToPath = 'dir/dir2/dir3'; +$zipOutputFile->addFilesFromRegex($inputDir, $regexPattern, $recursive = true, $moveToPath); ``` -Get archive comment +Add a files from [RegEx (Regular Expression) pattern](https://en.wikipedia.org/wiki/Regular_expression) to the archive and set compression method. ```php -$archiveComment = $zip->getArchiveComment(); +$compressionMethod = ZipEntry::METHOD_DEFLATED; +$zipOutputFile->addFilesFromRegex($inputDir, $regexPattern, $recursive, $moveToPath, $compressionMethod); ``` -Set archive comment +Rename entry name. ```php -$zip->setArchiveComment($comment) +$zipOutputFile->rename($oldName, $newName); ``` -Get comment file from index +Delete entry by name. ```php -$commentFile = $zip->getCommentIndex($index); +$zipOutputFile->deleteFromName($entryName); ``` -Set comment file from index +Delete entries from [glob pattern](https://en.wikipedia.org/wiki/Glob_(programming)). ```php -$zip->setCommentIndex($index, $comment); +$globPattern = '**.{jpg,jpeg,png,gif}'; // example glob pattern -> delete all .jpg, .jpeg, .png and .gif files +$zipOutputFile->deleteFromGlob($globPattern); ``` -Get comment file from filename +Delete entries from [RegEx (Regular Expression) pattern](https://en.wikipedia.org/wiki/Regular_expression). ```php -$commentFile = $zip->getCommentName($filename); +$regexPattern = '/\.(jpe?g|png|gif)$/si'; // example regex pattern -> delete all .jpg, .jpeg, .png and .gif files +$zipOutputFile->deleteFromRegex($regexPattern); ``` -Set comment file from filename +Delete all entries. ```php -$zip->setCommentName($name, $comment); +$zipOutputFile->deleteAll(); ``` -Get file content from index +Get num entries. ```php -$content = $zip->getFromIndex($index); +$count = $zipOutputFile->count(); +// or +$count = count($zipOutputFile); ``` -Get file content from filename +Get list files. ```php -$content = $zip->getFromName($name); +$listFiles = $zipOutputFile->getListFiles(); ``` -Get filename from index +Get the compression level for entries. ```php -$filename = $zip->getNameIndex($index); +$compressionLevel = $zipOutputFile->getLevel(); ``` -Rename file from index +Sets the compression level for entries. ```php -$zip->renameIndex($index, $newFilename); +// This property is only used if the effective compression method is DEFLATED or BZIP2. +// Legal values are ZipOutputFile::LEVEL_DEFAULT_COMPRESSION or range from +// ZipOutputFile::LEVEL_BEST_SPEED to ZipOutputFile::LEVEL_BEST_COMPRESSION. +$compressionMethod = ZipOutputFile::LEVEL_BEST_COMPRESSION; +$zipOutputFile->setLevel($compressionLevel); ``` -Rename file from filename +Get comment archive. ```php -$zip->renameName($oldName, $newName); +$commentArchive = $zipOutputFile->getComment(); ``` -Get zip entries +Set comment archive. ```php -/** - * @var \Nelexa\Zip\ZipEntry[] $zipEntries - */ -$zipEntries = $zip->getZipEntries(); +$zipOutputFile->setComment($commentArchive); ``` -Get zip entry from index +Get comment zip entry. ```php -/** - * @var \Nelexa\Zip\ZipEntry $zipEntry - */ -$zipEntry = $zip->getZipEntryIndex($index); +$commentEntry = $zipOutputFile->getEntryComment($entryName); ``` -Get zip entry from filename +Set comment zip entry. ```php -/** - * @var \Nelexa\Zip\ZipEntry $zipEntry - */ -$zipEntry = $zip->getZipEntryName($name); +$zipOutputFile->setEntryComment($entryName, $entryComment); ``` -Get info from index +Set compression method for zip entry. ```php -$info = $zip->statIndex($index); -// [ -// 'name' - filename -// 'index' - index number -// 'crc' - crc32 -// 'size' - uncompressed size -// 'mtime' - last modify date time -// 'comp_size' - compressed size -// 'comp_method' - compressed method -// ] -``` -Get info from name -```php -$info = $zip->statName($name); -// [ -// 'name' - filename -// 'index' - index number -// 'crc' - crc32 -// 'size' - uncompressed size -// 'mtime' - last modify date time -// 'comp_size' - compressed size -// 'comp_method' - compressed method -// ] -``` -Get info from all files -```php -$info = $zip->getExtendedListFiles(); -``` -Get output contents -```php -$content = $zip->output(); -``` -Save opened file -```php -$isSuccessSave = $zip->save(); -``` -Save file as -```php -$zip->saveAs($outputFile); -``` -Close archive -```php -$zip->close(); -``` +$compressionMethod = ZipEntry::METHOD_DEFLATED; +$zipOutputMethod->setCompressionMethod($entryName, $compressionMethod); -### Example create zip archive -```php -$zip = new \Nelexa\Zip\ZipFile(); -$zip->create(); -$zip->addFile("README.md"); -$zip->addFile("README.md", "folder/README"); -$zip->addFromString("folder/file.txt", "File content"); -$zip->addEmptyDir("f/o/l/d/e/r"); -$zip->setArchiveComment("Archive comment"); -$zip->setCommentIndex(0, "Comment file with index 0"); -$zip->saveAs("output.zip"); -$zip->close(); - -// $ zipinfo output.zip -// Archive: output.zip -// Zip file size: 912 bytes, number of entries: 4 -// -rw---- 1.0 fat 387 b- defN README.md -// -rw---- 1.0 fat 387 b- defN folder/README -// -rw---- 1.0 fat 12 b- defN folder/file.txt -// -rw---- 1.0 fat 0 b- stor f/o/l/d/e/r/ -// 4 files, 786 bytes uncompressed, 448 bytes compressed: 43.0% +// Support compression methods: +// ZipEntry::METHOD_STORED - no compression +// ZipEntry::METHOD_DEFLATED - deflate compression +// ZipEntry::METHOD_BZIP2 - bzip2 compression (need bz2 extension) ``` - -### Example modification zip archive +Set a password for all previously added entries. ```php -$zip = new \Nelexa\Zip\ZipFile(); -$zip->open("output.zip"); -$zip->addFromString("new-file", file_get_contents(__FILE__)); -$zip->saveAs("output2.zip"); -$zip->save(); -$zip->close(); +$zipOutputFile->setPassword($password); +``` +Set a password and encryption method for all previously added entries. +```php +$encryptionMethod = ZipEntry::ENCRYPTION_METHOD_WINZIP_AES; // default value +$zipOutputFile->setPassword($password, $encryptionMethod); -// $ zipinfo output2.zip -// Archive: output2.zip -// Zip file size: 1331 bytes, number of entries: 5 -// -rw---- 1.0 fat 387 b- defN README.md -// -rw---- 1.0 fat 387 b- defN folder/README -// -rw---- 1.0 fat 12 b- defN folder/file.txt -// -rw---- 1.0 fat 0 b- stor f/o/l/d/e/r/ -// -rw---- 1.0 fat 593 b- defN new-file -// 5 files, 1379 bytes uncompressed, 775 bytes compressed: 43.8% +// Support encryption methods: +// ZipEntry::ENCRYPTION_METHOD_TRADITIONAL - Traditional PKWARE Encryption +// ZipEntry::ENCRYPTION_METHOD_WINZIP_AES - WinZip AES Encryption +``` +Set a password for a concrete entry. +```php +$zipOutputFile->setEntryPassword($entryName, $password); +``` +Set a password and encryption method for a concrete entry. +```php +$zipOutputFile->setEntryPassword($entryName, $password, $encryptionMethod); + +// Support encryption methods: +// ZipEntry::ENCRYPTION_METHOD_TRADITIONAL - Traditional PKWARE Encryption +// ZipEntry::ENCRYPTION_METHOD_WINZIP_AES - WinZip AES Encryption (default value) +``` +Remove password from all entries. +```php +$zipOutputFile->removePasswordAllEntries(); +``` +Remove password for concrete zip entry. +```php +$zipOutputFile->removePasswordFromEntry($entryName); +``` +Save archive to a file. +```php +$zipOutputFile->saveAsFile($filename); +``` +Save archive to a stream. +```php +$handle = fopen($filename, 'w+b); +$autoCloseResource = true; +$zipOutputFile->saveAsStream($handle, $autoCloseResource); +if(!$autoCloseResource){ + fclose($handle); +} +``` +Returns the zip archive as a string. +```php +$rawZipArchiveBytes = $zipOutputFile->outputAsString(); +``` +Output .ZIP archive as attachment and terminate. +```php +$zipOutputFile->outputAsAttachment($outputFilename); +// or set mime type +$zipOutputFile->outputAsAttachment($outputFilename = 'output.zip', $mimeType = 'application/zip'); +``` +Extract all files to directory. +```php +$zipOutputFile->extractTo($directory); +``` +Extract some files to directory. +```php +$extractOnlyFiles = ["filename1", "filename2", "dir/dir/dir/"]; +$zipOutputFile->extractTo($directory, $extractOnlyFiles); +``` +Get entry contents. +```php +$data = $zipOutputFile->getEntryContent($entryName); +``` +Foreach zip entries. +```php +foreach($zipOutputFile as $entryName => $dataContent){ + echo "Entry: $entryName" . PHP_EOL; + echo "Data: $dataContent" . PHP_EOL; + echo "-----------------------------" . PHP_EOL; +} +``` +Iterator zip entries. +```php +$iterator = new \ArrayIterator($zipOutputFile); +while ($iterator->valid()) +{ + $entryName = $iterator->key(); + $dataContent = $iterator->current(); + + echo "Entry: $entryName" . PHP_EOL; + echo "Data: $dataContent" . PHP_EOL; + echo "-----------------------------" . PHP_EOL; + + $iterator->next(); +} +``` +Close zip archive. +```php +$zipOutputFile->close(); +``` +Examples +-------- +Create, open, extract and update archive. +```php +$outputFilename = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'output.zip'; +$outputDirExtract = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'extract'; + +if(!is_dir($outputDirExtract)){ + mkdir($outputDirExtract, 0755, true); +} + +$zipOutputFile = \PhpZip\ZipOutputFile::create(); // create archive +$zipOutputFile->addDir(__DIR__, true); // add this dir to archive +$zipOutputFile->saveAsFile($outputFilename); // save as file +$zipOutputFile->close(); // close output file, release all streams + +$zipFile = \PhpZip\ZipFile::openFromFile($outputFilename); // open zip archive from file +$zipFile->extractTo($outputDirExtract); // extract files to dir + +$zipOutputFile = \PhpZip\ZipOutputFile::openFromZipFile($zipFile); // create zip output archive for update +$zipOutputFile->deleteFromRegex('~^\.~'); // delete all hidden (Unix) files +$zipOutputFile->addFromString('dir/file.txt', 'Test file'); // add files from string contents +$zipOutputFile->saveAsFile($outputFilename); // update zip file +$zipOutputFile->close(); // close output file, release all streams + +$zipFile->close(); // close input file, release all streams +``` +Other examples can be found in the `tests/` folder + +Running Tests +------------- +```bash +vendor/bin/phpunit -v --tap -c bootstrap.xml ``` \ No newline at end of file diff --git a/bootstrap.xml b/bootstrap.xml new file mode 100644 index 0000000..25c557a --- /dev/null +++ b/bootstrap.xml @@ -0,0 +1,10 @@ + + + + + + ./tests + + + + \ No newline at end of file diff --git a/composer.json b/composer.json index 8ec18cf..5022a12 100644 --- a/composer.json +++ b/composer.json @@ -1,25 +1,37 @@ { "name": "nelexa/zip", - "description": "Zip create, modify and extract tool. Alternative ZipArchive.", + "description": "Zip files CRUD. Open, create, update, extract and get info tool. Support read and write encrypted archives. Support ZIP64 ext. Alternative ZipArchive. It does not require php-zip extension.", "type": "library", + "keywords": [ + "zip", + "archive", + "extract", + "winzip" + ], "require-dev": { - "phpunit/phpunit": "^5.5" + "phpunit/phpunit": "4.8" }, "license": "MIT", "authors": [ { "name": "Ne-Lexa", - "email": "alexey@nelexa.ru" + "email": "alexey@nelexa.ru", + "role": "Developer" } ], "minimum-stability": "stable", "require": { - "php": ">=5.3", - "nelexa/buffer": "^1.0" + "php-64bit": "^5.4 || ^7.0", + "ext-mbstring": "*" }, "autoload": { "psr-4": { - "Nelexa\\Zip\\": "src" + "": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "": "tests/" } } } diff --git a/src/FilterFileIterator.php b/src/FilterFileIterator.php deleted file mode 100644 index 7bc0fe4..0000000 --- a/src/FilterFileIterator.php +++ /dev/null @@ -1,45 +0,0 @@ -ignoreFiles = array_merge(self::$ignoreAlways, $ignoreFiles); - } - - /** - * (PHP 5 >= 5.1.0)
- * Check whether the current element of the iterator is acceptable - * @link http://php.net/manual/en/filteriterator.accept.php - * @return bool true if the current element is acceptable, otherwise false. - */ - public function accept() - { - /** - * @var \SplFileInfo $value - */ - $value = $this->current(); - $pathName = $value->getRealPath(); - foreach ($this->ignoreFiles AS $ignoreFile) { - if ($this->endsWith($pathName, $ignoreFile)) { - return false; - } - } - return true; - } - - function endsWith($haystack, $needle) - { - // search forward starting from end minus needle length characters - return $needle === "" || (($temp = strlen($haystack) - strlen($needle)) >= 0 && strpos($haystack, $needle, $temp) !== FALSE); - } -} \ No newline at end of file diff --git a/src/PhpZip/Crypto/TraditionalPkwareEncryptionEngine.php b/src/PhpZip/Crypto/TraditionalPkwareEncryptionEngine.php new file mode 100644 index 0000000..bd887fa --- /dev/null +++ b/src/PhpZip/Crypto/TraditionalPkwareEncryptionEngine.php @@ -0,0 +1,215 @@ +entry = $entry; + $this->initKeys($entry->getPassword()); + } + + /** + * Initial keys + * + * @param string $password + */ + private function initKeys($password) + { + $this->keys[0] = 305419896; + $this->keys[1] = 591751049; + $this->keys[2] = 878082192; + foreach (unpack('C*', $password) as $b) { + $this->updateKeys($b); + } + } + + /** + * Update keys. + * + * @param string $charAt + */ + private function updateKeys($charAt) + { + $this->keys[0] = self::crc32($this->keys[0], $charAt); + $this->keys[1] = ($this->keys[1] + ($this->keys[0] & 0xff)) & 4294967295; + $this->keys[1] = ($this->keys[1] * 134775813 + 1) & 4294967295; + $this->keys[2] = self::crc32($this->keys[2], ($this->keys[1] >> 24) & 0xff); + } + + /** + * Update crc. + * + * @param int $oldCrc + * @param string $charAt + * @return int + */ + private function crc32($oldCrc, $charAt) + { + return (($oldCrc >> 8) & 0xffffff) ^ self::$CRC_TABLE[($oldCrc ^ $charAt) & 0xff]; + } + + /** + * @param string $content + * @return string + * @throws ZipAuthenticationException + */ + public function decrypt($content) + { + $headerBytes = array_values(unpack('C*', substr($content, 0, self::STD_DEC_HDR_SIZE))); + foreach ($headerBytes as &$byte) { + $byte = ($byte ^ $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->getRawTime() >> 8) & 0xff; + } else { + // compare against the CRC otherwise + $checkByte = ($this->entry->getCrc() >> 24) & 0xff; + } + if ($headerBytes[11] !== $checkByte) { + throw new ZipAuthenticationException("Bad password for entry " . $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 + * @param int $crc + * @return string + */ + public function encrypt($data, $crc) + { + $headerBytes = CryptoUtil::randomBytes(self::STD_DEC_HDR_SIZE); + + // Initialize again since the generated bytes were encrypted. + $this->initKeys($this->entry->getPassword()); + $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 + * @return string + */ + private function encryptData($content) + { + if ($content === null) { + throw new \RuntimeException(); + } + $buff = ''; + foreach (unpack('C*', $content) as $val) { + $buff .= pack('c', $this->encryptByte($val)); + } + return $buff; + } + + /** + * @param int $byte + * @return int + */ + protected function encryptByte($byte) + { + $tempVal = $byte ^ $this->decryptByte() & 0xff; + $this->updateKeys($byte); + return $tempVal; + } +} \ No newline at end of file diff --git a/src/PhpZip/Crypto/WinZipAesEngine.php b/src/PhpZip/Crypto/WinZipAesEngine.php new file mode 100644 index 0000000..0283dfb --- /dev/null +++ b/src/PhpZip/Crypto/WinZipAesEngine.php @@ -0,0 +1,231 @@ +entry = $entry; + } + + /** + * Decrypt from stream resource. + * + * @param resource $stream Input stream resource + * @return string + * @throws ZipAuthenticationException + * @throws ZipCryptoException + */ + public function decrypt($stream) + { + /** + * @var WinZipAesEntryExtraField $field + */ + $field = $this->entry->getExtraField(WinZipAesEntryExtraField::getHeaderId()); + if (null === $field) { + throw new ZipCryptoException($this->entry->getName() . " (missing extra field for WinZip AES entry)"); + } + + $pos = ftell($stream); + + // Get key strength. + $keyStrengthBits = $field->getKeyStrength(); + $keyStrengthBytes = $keyStrengthBits / 8; + + $salt = fread($stream, $keyStrengthBytes / 2); + $passwordVerifier = fread($stream, self::PWD_VERIFIER_BITS / 8); + + $sha1Size = 20; + + // Init start, end and size of encrypted data. + $endPos = $pos + $this->entry->getCompressedSize(); + $start = ftell($stream); + $footerSize = $sha1Size / 2; + $end = $endPos - $footerSize; + $size = $end - $start; + + if (0 > $size) { + throw new ZipCryptoException($this->entry->getName() . " (false positive WinZip AES entry is too short)"); + } + + // Load authentication code. + fseek($stream, $end, SEEK_SET); + $authenticationCode = fread($stream, $footerSize); + if (ftell($stream) !== $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!"); + } + + do { + assert($this->entry->getPassword() !== null); + assert(self::AES_BLOCK_SIZE_BITS <= $keyStrengthBits); + + // 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", $this->entry->getPassword(), $salt, self::ITERATION_COUNT, (2 * $keyStrengthBits + self::PWD_VERIFIER_BITS) / 8, true); + $ctrIvSize = self::AES_BLOCK_SIZE_BITS / 8; + $iv = str_repeat(chr(0), $ctrIvSize); + + $key = substr($keyParam, 0, $keyStrengthBytes); + + $sha1MacParam = substr($keyParam, $keyStrengthBytes, $keyStrengthBytes); + // Verify password. + } while (!$passwordVerifier === substr($keyParam, 2 * $keyStrengthBytes)); + + $content = stream_get_contents($stream, $size, $start); + $mac = hash_hmac('sha1', $content, $sha1MacParam, true); + + if ($authenticationCode !== substr($mac, 0, 10)) { + throw new ZipAuthenticationException($this->entry->getName() . " (authenticated WinZip AES entry content has been tampered with)"); + } + + return self::aesCtrSegmentIntegerCounter(false, $content, $key, $iv); + } + + /** + * Decryption or encryption AES-CTR with Segment Integer Count (SIC). + * + * @param bool $encrypted If true encryption else decryption + * @param string $str Data + * @param string $key Key + * @param string $iv IV + * @return string + */ + private static function aesCtrSegmentIntegerCounter($encrypted = true, $str, $key, $iv) + { + $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; + return openssl_encrypt($data, 'AES-' . $numBits . '-CTR', $key, OPENSSL_RAW_DATA, $iv); + } elseif (extension_loaded("mcrypt")) { + return mcrypt_encrypt(MCRYPT_RIJNDAEL_128, $key, $data, "ctr", $iv); + } else { + 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; + return openssl_decrypt($data, 'AES-' . $numBits . '-CTR', $key, OPENSSL_RAW_DATA, $iv); + } elseif (extension_loaded("mcrypt")) { + return mcrypt_decrypt(MCRYPT_RIJNDAEL_128, $key, $data, "ctr", $iv); + } else { + throw new \RuntimeException('Extension openssl or mcrypt not loaded'); + } + } + + /** + * Encryption string. + * + * @param string $content + * @return string + */ + public function encrypt($content) + { + // Init key strength. + $password = $this->entry->getPassword(); + assert($password !== null); + + $keyStrengthBytes = 32; + $keyStrengthBits = $keyStrengthBytes * 8; + + assert(self::AES_BLOCK_SIZE_BITS <= $keyStrengthBits); + + $salt = CryptoUtil::randomBytes($keyStrengthBytes / 2); + + $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(true, $content, $key, $iv); + + $mac = hash_hmac('sha1', $content, $sha1HMacParam, true); + + return ($salt . + substr($keyParam, 2 * $keyStrengthBytes, self::PWD_VERIFIER_BITS / 8) . + $content . + substr($mac, 0, 10) + ); + } +} \ No newline at end of file diff --git a/src/PhpZip/Exception/Crc32Exception.php b/src/PhpZip/Exception/Crc32Exception.php new file mode 100644 index 0000000..f344fa5 --- /dev/null +++ b/src/PhpZip/Exception/Crc32Exception.php @@ -0,0 +1,70 @@ +expectedCrc = $expected; + $this->actualCrc = $actual; + } + + /** + * Returns expected crc. + * + * @return int + */ + public function getExpectedCrc() + { + return $this->expectedCrc; + } + + /** + * Returns actual crc. + * + * @return int + */ + public function getActualCrc() + { + return $this->actualCrc; + } + +} \ No newline at end of file diff --git a/src/PhpZip/Exception/IllegalArgumentException.php b/src/PhpZip/Exception/IllegalArgumentException.php new file mode 100644 index 0000000..44d9578 --- /dev/null +++ b/src/PhpZip/Exception/IllegalArgumentException.php @@ -0,0 +1,14 @@ + $headerId || $headerId > 0xffff) { + throw new ZipException('headerId out of range'); + } + self::$headerId = $headerId; + } + + /** + * Returns the Header ID (type) of this Extra Field. + * The Header ID is an unsigned short integer (two bytes) + * which must be constant during the life cycle of this object. + * + * @return int + */ + public static function getHeaderId() + { + return self::$headerId & 0xffff; + } + + /** + * Returns the Data Size of this Extra Field. + * The Data Size is an unsigned short integer (two bytes) + * which indicates the length of the Data Block in bytes and does not + * include its own size in this Extra Field. + * This property may be initialized by calling ExtraField::readFrom. + * + * @return int The size of the Data Block in bytes + * or 0 if unknown. + */ + public function getDataSize() + { + return null !== $this->data ? strlen($this->data) : 0; + } + + /** + * Initializes this Extra Field by deserializing a Data Block of + * size bytes $size from the resource $handle at the zero based offset $off. + * + * @param resource $handle + * @param int $off Offset bytes + * @param int $size Size + * @throws ZipException + */ + public function readFrom($handle, $off, $size) + { + if (0x0000 > $size || $size > 0xffff) { + throw new ZipException('size out of range'); + } + if ($size > 0) { + fseek($handle, $off, SEEK_SET); + $this->data = fread($handle, $size); + } + } + + /** + * @param resource $handle + * @param int $off + */ + public function writeTo($handle, $off) + { + if (null !== $this->data) { + fseek($handle, $off, SEEK_SET); + fwrite($handle, $this->data); + } + } +} \ No newline at end of file diff --git a/src/PhpZip/Extra/ExtraField.php b/src/PhpZip/Extra/ExtraField.php new file mode 100644 index 0000000..4927f5f --- /dev/null +++ b/src/PhpZip/Extra/ExtraField.php @@ -0,0 +1,120 @@ + $headerId || $headerId > 0xffff) { + throw new ZipException('headerId out of range'); + } + + /** + * @var ExtraField $extraField + */ + if (isset(self::getRegistry()[$headerId])) { + $extraClassName = self::getRegistry()[$headerId]; + $extraField = new $extraClassName; + if ($headerId !== $extraField::getHeaderId()) { + throw new ZipException('Runtime error support headerId ' . $headerId); + } + } else { + $extraField = new DefaultExtraField($headerId); + } + return $extraField; + } + + /** + * Registered extra field classes. + * + * @return array|null + */ + private static function getRegistry() + { + if (self::$registry === null) { + self::$registry[WinZipAesEntryExtraField::getHeaderId()] = '\PhpZip\Extra\WinZipAesEntryExtraField'; + self::$registry[NtfsExtraField::getHeaderId()] = '\PhpZip\Extra\NtfsExtraField'; + } + return self::$registry; + } + + /** + * Returns a protective copy of the Data Block. + * + * @return resource + * @throws ZipException If size data block out of range. + */ + public function getDataBlock() + { + $size = $this->getDataSize(); + if (0x0000 > $size || $size > 0xffff) { + throw new ZipException('size data block out of range.'); + } + $fp = fopen('php://temp', 'r+b'); + if (0 === $size) return $fp; + $this->writeTo($fp, 0); + rewind($fp); + return $fp; + } + + /** + * Returns the Data Size of this Extra Field. + * The Data Size is an unsigned short integer (two bytes) + * which indicates the length of the Data Block in bytes and does not + * include its own size in this Extra Field. + * This property may be initialized by calling ExtraField::readFrom. + * + * @return int The size of the Data Block in bytes + * or 0 if unknown. + */ + abstract public function getDataSize(); + + /** + * Serializes a Data Block of ExtraField::getDataSize bytes to the + * resource $handle at the zero based offset $off. + * + * @param resource $handle + * @param int $off Offset bytes + */ + abstract public function writeTo($handle, $off); + + /** + * Initializes this Extra Field by deserializing a Data Block of + * size bytes $size from the resource $handle at the zero based offset $off. + * + * @param resource $handle + * @param int $off Offset bytes + * @param int $size Size + */ + abstract public function readFrom($handle, $off, $size); +} \ No newline at end of file diff --git a/src/PhpZip/Extra/ExtraFieldHeader.php b/src/PhpZip/Extra/ExtraFieldHeader.php new file mode 100644 index 0000000..f586e5a --- /dev/null +++ b/src/PhpZip/Extra/ExtraFieldHeader.php @@ -0,0 +1,21 @@ +extra); + } + + /** + * Returns the Extra Field with the given Header ID or null + * if no such Extra Field exists. + * + * @param int $headerId The requested Header ID. + * @return ExtraField The Extra Field with the given Header ID or + * if no such Extra Field exists. + * @throws ZipException If headerId is out of range. + */ + public function get($headerId) + { + if (0x0000 > $headerId || $headerId > 0xffff) { + throw new ZipException('headerId out of range'); + } + if (isset($this->extra[$headerId])) { + return $this->extra[$headerId]; + } + return null; + } + + /** + * Stores the given Extra Field in this collection. + * + * @param ExtraField $extraField The Extra Field to store in this collection. + * @return ExtraField The Extra Field previously associated with the Header ID of + * of the given Extra Field or null if no such Extra Field existed. + * @throws ZipException If headerId is out of range. + */ + public function add(ExtraField $extraField) + { + $headerId = $extraField::getHeaderId(); + if (0x0000 > $headerId || $headerId > 0xffff) { + throw new ZipException('headerId out of range'); + } + $this->extra[$headerId] = $extraField; + return $extraField; + } + + /** + * Returns Extra Field exists + * + * @param int $headerId The requested Header ID. + * @return bool + */ + public function has($headerId) + { + return isset($this->extra[$headerId]); + } + + /** + * Removes the Extra Field with the given Header ID. + * + * @param int $headerId The requested Header ID. + * @return ExtraField The Extra Field with the given Header ID or null + * if no such Extra Field exists. + * @throws ZipException If headerId is out of range or extra field not found. + */ + public function remove($headerId) + { + if (0x0000 > $headerId || $headerId > 0xffff) { + throw new ZipException('headerId out of range'); + } + if (isset($this->extra[$headerId])) { + $ef = $this->extra[$headerId]; + unset($this->extra[$headerId]); + return $ef; + } + throw new ZipException('ExtraField not found'); + } + + /** + * Returns a protective copy of the Extra Fields. + * null is never returned. + * + * @return string + * @throws ZipException If size out of range + */ + public function getExtra() + { + $size = $this->getExtraLength(); + if (0x0000 > $size || $size > 0xffff) { + throw new ZipException('size out of range'); + } + if (0 === $size) return ''; + + $fp = fopen('php://temp', 'r+b'); + $this->writeTo($fp, 0); + rewind($fp); + $content = stream_get_contents($fp); + fclose($fp); + return $content; + } + + /** + * Returns the number of bytes required to hold the Extra Fields. + * + * @return int The length of the Extra Fields in bytes. May be 0. + * @see #getExtra + */ + public function getExtraLength() + { + if (empty($this->extra)) { + return 0; + } + $length = 0; + + /** + * @var ExtraField $extraField + */ + foreach ($this->extra as $extraField) { + $length += 4 + $extraField->getDataSize(); + } + return $length; + } + + /** + * Serializes a list of Extra Fields of ExtraField::getExtraLength bytes to the + * stream resource $handle at the zero based offset $off. + * + * @param resource $handle + * @param int $off Offset + */ + private function writeTo($handle, $off) + { + fseek($handle, $off, SEEK_SET); + /** + * @var ExtraField $ef + */ + foreach ($this->extra as $ef) { + fwrite($handle, pack('vv', $ef::getHeaderId(), $ef->getDataSize())); + $off += 4; + fwrite($handle, $ef->writeTo($handle, $off)); + $off += $ef->getDataSize(); + } + } + + /** + * Initializes this Extra Field by deserializing a Data Block of + * size bytes $size from the resource $handle at the zero based offset $off. + * + * @param resource $handle + * @param int $off Offset + * @param int $size Size + * @throws ZipException If size out of range + */ + public function readFrom($handle, $off, $size) + { + if (0x0000 > $size || $size > 0xffff) { + throw new ZipException('size out of range'); + } + $map = []; + if (null !== $handle && 0 < $size) { + $end = $off + $size; + while ($off < $end) { + fseek($handle, $off, SEEK_SET); + $unpack = unpack('vheaderId/vdataSize', fread($handle, 4)); + $off += 4; + $extraField = ExtraField::create($unpack['headerId']); + $extraField->readFrom($handle, $off, $unpack['dataSize']); + $off += $unpack['dataSize']; + $map[$unpack['headerId']] = $extraField; + } + assert($off === $end); + } + $this->extra = $map; + } + + /** + * If clone extra fields. + */ + function __clone() + { + foreach ($this->extra as $k => $v) { + $this->extra[$k] = clone $v; + } + } + +} \ No newline at end of file diff --git a/src/PhpZip/Extra/NtfsExtraField.php b/src/PhpZip/Extra/NtfsExtraField.php new file mode 100644 index 0000000..914ff46 --- /dev/null +++ b/src/PhpZip/Extra/NtfsExtraField.php @@ -0,0 +1,176 @@ +rawData); + } + + /** + * Initializes this Extra Field by deserializing a Data Block of + * size bytes $size from the resource $handle at the zero based offset $off. + * + * @param resource $handle + * @param int $off Offset bytes + * @param int $size Size + * @throws ZipException If size out of range + */ + public function readFrom($handle, $off, $size) + { + if (0x0000 > $size || $size > 0xffff) { + throw new ZipException('size out of range'); + } + if ($size > 0) { + $off += 4; + fseek($handle, $off, SEEK_SET); + + $unpack = unpack('vtag/vsizeAttr', fread($handle, 4)); + if ($unpack['sizeAttr'] === 24) { + $tagData = fread($handle, $unpack['sizeAttr']); + + $this->mtime = PackUtil::unpackLongLE(substr($tagData, 0, 8)) / 10000000 - 11644473600; + $this->atime = PackUtil::unpackLongLE(substr($tagData, 8, 8)) / 10000000 - 11644473600; + $this->ctime = PackUtil::unpackLongLE(substr($tagData, 16, 8)) / 10000000 - 11644473600; + } + $off += $unpack['sizeAttr']; + + if ($size > $off) { + $this->rawData .= fread($handle, $size - $off); + } + } + } + + /** + * Serializes a Data Block of ExtraField::getDataSize bytes to the + * resource $handle at the zero based offset $off. + * + * @param resource $handle + * @param int $off Offset bytes + */ + public function writeTo($handle, $off) + { + if ($this->mtime !== null && $this->atime !== null && $this->ctime !== null) { + fseek($handle, $off, SEEK_SET); + fwrite($handle, pack('Vvv', 0, 1, 8 * 3 + strlen($this->rawData))); + $mtimeLong = ($this->mtime + 11644473600) * 10000000; + fwrite($handle, PackUtil::packLongLE($mtimeLong)); + $atimeLong = ($this->atime + 11644473600) * 10000000; + fwrite($handle, PackUtil::packLongLE($atimeLong)); + $ctimeLong = ($this->ctime + 11644473600) * 10000000; + fwrite($handle, PackUtil::packLongLE($ctimeLong)); + if (!empty($this->rawData)) { + fwrite($handle, $this->rawData); + } + } + } + + /** + * @return int + */ + public function getMtime() + { + return $this->mtime; + } + + /** + * @param int $mtime + */ + public function setMtime($mtime) + { + $this->mtime = (int)$mtime; + } + + /** + * @return int + */ + public function getAtime() + { + return $this->atime; + } + + /** + * @param int $atime + */ + public function setAtime($atime) + { + $this->atime = (int)$atime; + } + + /** + * @return int + */ + public function getCtime() + { + return $this->ctime; + } + + /** + * @param int $ctime + */ + public function setCtime($ctime) + { + $this->ctime = (int)$ctime; + } + +} \ No newline at end of file diff --git a/src/PhpZip/Extra/WinZipAesEntryExtraField.php b/src/PhpZip/Extra/WinZipAesEntryExtraField.php new file mode 100644 index 0000000..6f32ce2 --- /dev/null +++ b/src/PhpZip/Extra/WinZipAesEntryExtraField.php @@ -0,0 +1,236 @@ +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; + + private static $keyStrengths = [ + self::KEY_STRENGTH_128BIT => 0x01, + self::KEY_STRENGTH_192BIT => 0x02, + self::KEY_STRENGTH_256BIT => 0x03 + ]; + + /** + * Vendor version. + * + * @var int + */ + private $vendorVersion = self::VV_AE_1; + + /** + * Encryption strength. + * + * @var int + */ + private $encryptionStrength = self::KEY_STRENGTH_256BIT; + + /** + * Zip compression method. + * + * @var int + */ + private $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 Data Size of this Extra Field. + * The Data Size is an unsigned short integer (two bytes) + * which indicates the length of the Data Block in bytes and does not + * include its own size in this Extra Field. + * This property may be initialized by calling ExtraField::readFrom. + * + * @return int The size of the Data Block in bytes + * or 0 if unknown. + */ + public function getDataSize() + { + return self::DATA_SIZE; + } + + /** + * Returns the vendor version. + * + * @see WinZipAesEntryExtraField::VV_AE_1 + * @see WinZipAesEntryExtraField::VV_AE_2 + */ + public function getVendorVersion() + { + return $this->vendorVersion & 0xffff; + } + + /** + * Sets the vendor version. + * + * @see WinZipAesEntryExtraField::VV_AE_1 + * @see WinZipAesEntryExtraField::VV_AE_2 + * @param int $vendorVersion the vendor version. + * @throws ZipException Unsupport vendor version. + */ + public function setVendorVersion($vendorVersion) + { + if ($vendorVersion < self::VV_AE_1 || self::VV_AE_2 < $vendorVersion) { + throw new ZipException($vendorVersion); + } + $this->vendorVersion = $vendorVersion; + } + + /** + * Returns vendor id. + * + * @return int + */ + public function getVendorId() + { + return self::VENDOR_ID; + } + + /** + * @return bool|int + */ + public function getKeyStrength() + { + return self::keyStrength($this->encryptionStrength); + } + + /** + * @param int $encryptionStrength Encryption strength as bits. + * @return int + * @throws ZipException If unsupport encryption strength. + */ + 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; + } + + /** + * Sets compression method. + * + * @param int $compressionMethod Compression method + * @throws ZipException Compression method out of range. + */ + public function setMethod($compressionMethod) + { + if (0x0000 > $compressionMethod || $compressionMethod > 0xffff) { + throw new ZipException('Compression method out of range'); + } + $this->method = $compressionMethod; + } + + /** + * Initializes this Extra Field by deserializing a Data Block of + * size bytes $size from the resource $handle at the zero based offset $off. + * + * @param resource $handle + * @param int $off Offset bytes + * @param int $size Size + * @throws ZipException + */ + public function readFrom($handle, $off, $size) + { + if (self::DATA_SIZE != $size) + throw new ZipException(); + + fseek($handle, $off, SEEK_SET); + /** + * @var int $vendorVersion + * @var int $vendorId + * @var int $keyStrength + * @var int $method + */ + $unpack = unpack('vvendorVersion/vvendorId/ckeyStrength/vmethod', fread($handle, 7)); + extract($unpack); + $this->setVendorVersion($vendorVersion); + if (self::VENDOR_ID != $vendorId) { + throw new ZipException(); + } + $this->setKeyStrength(self::keyStrength($keyStrength)); // checked + $this->setMethod($method); + } + + /** + * Set key strength. + * + * @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 of ExtraField::getDataSize bytes to the + * resource $handle at the zero based offset $off. + * + * @param resource $handle + * @param int $off Offset bytes + */ + public function writeTo($handle, $off) + { + fseek($handle, $off, SEEK_SET); + fwrite($handle, pack('vvcv', $this->vendorVersion, self::VENDOR_ID, $this->encryptionStrength, $this->method)); + } +} \ No newline at end of file diff --git a/src/PhpZip/Mapper/OffsetPositionMapper.php b/src/PhpZip/Mapper/OffsetPositionMapper.php new file mode 100644 index 0000000..038cc47 --- /dev/null +++ b/src/PhpZip/Mapper/OffsetPositionMapper.php @@ -0,0 +1,42 @@ +offset = $offset; + } + + /** + * @param int $position + * @return int + */ + public function map($position) + { + return parent::map($position) + $this->offset; + } + + /** + * @param int $position + * @return int + */ + public function unmap($position) + { + return parent::unmap($position) - $this->offset; + } +} \ No newline at end of file diff --git a/src/PhpZip/Mapper/PositionMapper.php b/src/PhpZip/Mapper/PositionMapper.php new file mode 100644 index 0000000..a7b02a4 --- /dev/null +++ b/src/PhpZip/Mapper/PositionMapper.php @@ -0,0 +1,29 @@ +setName($name); + } + + /** + * Detect current platform + * + * @return int + */ + public static function getCurrentPlatform() + { + if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') { + return self::PLATFORM_FAT; + } elseif (PHP_OS === 'Darwin') { + return self::PLATFORM_OS_X; + } else { + return self::PLATFORM_UNIX; + } + } + + /** + * Clone extra fields + */ + function __clone() + { + $this->fields = $this->fields !== null ? clone $this->fields : null; + } + + /** + * Returns the ZIP entry name. + * + * @return string + */ + public function getName() + { + return $this->name; + } + + /** + * Set entry name. + * + * @see ZipEntry::__construct + * @see ZipOutputFile::rename() + * + * @param string $name New entry name + * @throws ZipException + */ + public function setName($name) + { + $length = strlen($name); + if (0x0000 > $length || $length > 0xffff) { + throw new ZipException('Illegal zip entry name parameter'); + } + $encoding = mb_detect_encoding($this->name, "ASCII, UTF-8", true); + $this->setGeneralPurposeBitFlag(self::GPBF_UTF8, $encoding === 'UTF-8'); + $this->name = $name; + } + + /** + * Get platform + * + * @return int + */ + public function getPlatform() + { + return $this->isInit(self::BIT_PLATFORM) ? $this->platform & 0xffff : self::UNKNOWN; + } + + /** + * Set platform + * + * @param int $platform + * @throws ZipException + */ + public function setPlatform($platform) + { + $known = self::UNKNOWN !== $platform; + if ($known) { + if (0x00 > $platform || $platform > 0xff) { + throw new ZipException("Platform out of range"); + } + $this->platform = $platform; + } else { + $this->platform = 0; + } + $this->setInit(self::BIT_PLATFORM, $known); + } + + /** + * @param int $mask + * @return bool + */ + private function isInit($mask) + { + return 0 !== ($this->init & $mask); + } + + /** + * @param int $mask + * @param bool $init + */ + private function setInit($mask, $init) + { + if ($init) { + $this->init |= $mask; + } else { + $this->init &= ~$mask; + } + } + + /** + * @return int + */ + public function getRawPlatform() + { + return $this->platform & 0xff; + } + + /** + * @param int $platform + * @throws ZipException + */ + public function setRawPlatform($platform) + { + if (0x00 > $platform || $platform > 0xff) { + throw new ZipException("Platform out of range"); + } + $this->platform = $platform; + $this->setInit(self::BIT_PLATFORM, true); + } + + /** + * Version needed to extract. + * + * @return int + */ + public function getVersionNeededToExtract() + { + $method = $this->getRawMethod(); + return self::WINZIP_AES === $method ? 51 : + (self::METHOD_BZIP2 === $method ? 46 : + ($this->isZip64ExtensionsRequired() ? 45 : + (self::METHOD_DEFLATED === $method || $this->isDirectory() ? 20 : 10 + ) + ) + ); + } + + /** + * @return int + */ + public function getRawMethod() + { + return $this->method & 0xff; + } + + /** + * @return bool + */ + public function isZip64ExtensionsRequired() + { + // Offset MUST be considered in decision about ZIP64 format - see + // description of Data Descriptor in ZIP File Format Specification! + return 0xffffffff <= $this->getCompressedSize() + || 0xffffffff <= $this->getSize() + || 0xffffffff <= $this->getOffset(); + } + + /** + * 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. + * @throws ZipException + */ + public function setCompressedSize($compressedSize) + { + if (self::UNKNOWN != $compressedSize) { + if (0 > $compressedSize || $compressedSize > 0x7fffffffffffffff) { + throw new ZipException("Compressed size out of range - " . $this->name); + } + } + $this->compressedSize = $compressedSize; + } + + /** + * Returns the uncompressed size of this entry. + * + * @see #setCompressedSize + */ + public function getSize() + { + return $this->size; + } + + /** + * Sets the uncompressed size of this entry. + * + * @param int $size The (Uncompressed) Size. + * @throws ZipException + */ + public function setSize($size) + { + if (self::UNKNOWN != $size) { + if (0 > $size || $size > 0x7fffffffffffffff) { + throw new ZipException("Uncompressed Size out of range - " . $this->name); + } + } + $this->size = $size; + } + + /** + * Return relative Offset Of Local File Header. + * + * @return int + */ + public function getOffset() + { + return $this->offset; + } + + /** + * Returns true if and only if this ZIP entry represents a directory entry + * (i.e. end with '/'). + * + * @return bool + */ + public function isDirectory() + { + return $this->name[strlen($this->name) - 1] === '/'; + } + + /** + * Returns the General Purpose Bit Flags. + * + * @return bool + */ + public function getGeneralPurposeBitFlags() + { + return $this->general & 0xffff; + } + + /** + * Sets the General Purpose Bit Flags. + * + * @var int general + * @throws ZipException + */ + public function setGeneralPurposeBitFlags($general) + { + if (0x0000 > $general || $general > 0xffff) { + throw new ZipException('general out of range'); + } + $this->general = $general; + } + + /** + * 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 0 !== ($this->general & $mask); + } + + /** + * Sets the encryption property to false and removes any other + * encryption artifacts. + */ + public function clearEncryption() + { + $this->setEncrypted(false); + $field = $this->fields->get(WinZipAesEntryExtraField::getHeaderId()); + if ($field !== null) { + /** + * @var WinZipAesEntryExtraField $field + */ + $this->removeExtraField(WinZipAesEntryExtraField::getHeaderId()); + } + if (self::WINZIP_AES === $this->getRawMethod()) { + $this->setRawMethod(null === $field ? self::UNKNOWN : $field->getMethod()); + } + $this->password = null; + } + + /** + * Sets the encryption flag for this ZIP entry. + * + * @param bool $encrypted + */ + public function setEncrypted($encrypted) + { + $this->setGeneralPurposeBitFlag(self::GPBF_ENCRYPTED, $encrypted); + } + + /** + * Sets the indexed General Purpose Bit Flag. + * + * @param int $mask + * @param bool $bit + */ + public function setGeneralPurposeBitFlag($mask, $bit) + { + if ($bit) + $this->general |= $mask; + else + $this->general &= ~$mask; + } + + /** + * Remove extra field from header id. + * + * @param int $headerId + * @return ExtraField|null + */ + public function removeExtraField($headerId) + { + return null !== $this->fields ? $this->fields->remove($headerId) : null; + } + + /** + * @param int $method + * @throws ZipException + */ + public function setRawMethod($method) + { + if (0x0000 > $method || $method > 0xffff) { + throw new ZipException('method out of range'); + } + $this->setMethod($method); + } + + /** + * Returns the compression method for this entry. + * + * @return int + */ + public function getMethod() + { + return $this->isInit(self::BIT_METHOD) ? $this->method & 0xffff : self::UNKNOWN; + } + + /** + * Sets the compression method for this entry. + * + * @param int $method + * @throws ZipException If method is not STORED, DEFLATED, BZIP2 or UNKNOWN. + */ + public function setMethod($method) + { + switch ($method) { + case self::WINZIP_AES: + $this->method = $method; + $this->setInit(self::BIT_METHOD, true); + $this->setEncryptionMethod(self::ENCRYPTION_METHOD_WINZIP_AES); + break; + + case self::METHOD_STORED: + case self::METHOD_DEFLATED: + case self::METHOD_BZIP2: + $this->method = $method; + $this->setInit(self::BIT_METHOD, true); + break; + + case self::UNKNOWN: + $this->method = 0; + $this->setInit(self::BIT_METHOD, false); + break; + + default: + throw new ZipException($this->name . " (unsupported compression method $method)"); + } + } + + /** + * Get Unix Timestamp + * + * @return int + */ + public function getTime() + { + if (!$this->isInit(self::BIT_DATE_TIME)) { + return self::UNKNOWN; + } + return DateTimeConverter::toUnixTimestamp($this->dosTime & 0xffffffff); + } + + /** + * Set time from unix timestamp. + * + * @param int $unixTimestamp + */ + public function setTime($unixTimestamp) + { + $known = self::UNKNOWN != $unixTimestamp; + if ($known) { + $this->dosTime = DateTimeConverter::toDosTime($unixTimestamp); + } else { + $this->dosTime = 0; + } + $this->setInit(self::BIT_DATE_TIME, $known); + } + + /** + * @return int + */ + public function getRawTime() + { + return $this->dosTime & 0xffffffff; + } + + /** + * @param int $dtime + * @throws ZipException + */ + public function setRawTime($dtime) + { + if (0x00000000 > $dtime || $dtime > 0xffffffff) { + throw new ZipException('dtime out of range'); + } + $this->dosTime = $dtime; + $this->setInit(self::BIT_DATE_TIME, true); + } + + /** + * @return int + */ + public function getRawCrc() + { + return $this->crc & 0xffffffff; + } + + /** + * @param int $crc + * @throws ZipException + */ + public function setRawCrc($crc) + { + if (0x00000000 > $crc || $crc > 0xffffffff) { + throw new ZipException("CRC-32 out of range - " . $this->name); + } + $this->crc = $crc; + $this->setInit(self::BIT_CRC, true); + } + + /** + * Returns the external file attributes. + * + * @return int The external file attributes. + */ + public function getExternalAttributes() + { + return $this->isInit(self::BIT_EXTERNAL_ATTR) ? $this->externalAttributes & 0xffffffff : self::UNKNOWN; + } + + /** + * Sets the external file attributes. + * + * @param int $externalAttributes the external file attributes. + * @throws ZipException + */ + public function setExternalAttributes($externalAttributes) + { + $known = self::UNKNOWN != $externalAttributes; + if ($known) { + if (0x00000000 > $externalAttributes || $externalAttributes > 0xffffffff) { + throw new ZipException("external file attributes out of range - " . $this->name); + } + $this->externalAttributes = $externalAttributes; + } else { + $this->externalAttributes = 0; + } + $this->setInit(self::BIT_EXTERNAL_ATTR, $known); + } + + /** + * @return int + */ + public function getRawExternalAttributes() + { + if (!$this->isInit(self::BIT_EXTERNAL_ATTR)) { + return $this->isDirectory() ? 0x10 : 0; + } + return $this->externalAttributes & 0xffffffff; + } + + /** + * @param int $externalAttributes + * @throws ZipException + */ + public function setRawExternalAttributes($externalAttributes) + { + if (0x00000000 > $externalAttributes || $externalAttributes > 0xffffffff) { + throw new ZipException("external file attributes out of range - " . $this->name); + } + $this->externalAttributes = $externalAttributes; + $this->setInit(self::BIT_EXTERNAL_ATTR, true); + } + + /** + * Return extra field from header id. + * + * @param int $headerId + * @return ExtraField|null + */ + public function getExtraField($headerId) + { + return $this->fields === null ? null : $this->fields->get($headerId); + } + + /** + * Return exists extra field from header id. + * + * @param int $headerId + * @return bool + */ + public function hasExtraField($headerId) + { + return $this->fields === null ? false : $this->fields->has($headerId); + } + + /** + * Add extra field. + * + * @param ExtraField $field + * @return ExtraField + * @throws ZipException + */ + public function addExtraField($field) + { + if (null === $field) { + throw new ZipException("extra field null"); + } + if (null === $this->fields) { + $this->fields = new ExtraFields(); + } + return $this->fields->add($field); + } + + /** + * Returns a protective copy of the serialized Extra Fields. + * + * @return string A new byte array holding the serialized Extra Fields. + * null is never returned. + */ + public function getExtra() + { + return $this->getExtraFields(false); + } + + /** + * @param bool $zip64 + * @return bool|string + * @throws ZipException + */ + private function getExtraFields($zip64) + { + if ($zip64) { + $field = $this->composeZip64ExtraField(); + if (null !== $field) { + if (null === $this->fields) { + $this->fields = new ExtraFields(); + } + $this->fields->add($field); + } + } else { + assert(null === $this->fields || null === $this->fields->get(ExtraField::ZIP64_HEADER_ID)); + } + return null === $this->fields ? null : $this->fields->getExtra(); + } + + /** + * Composes a ZIP64 Extended Information Extra Field from the properties + * of this entry. + * If no ZIP64 Extended Information Extra Field is required it is removed + * from the collection of Extra Fields. + * + * @return ExtraField|null + */ + private function composeZip64ExtraField() + { + $off = 0; + $fp = fopen('php://temp', 'r+b'); + // Write out Uncompressed Size. + $size = $this->getSize(); + if (0xffffffff <= $size) { + fseek($fp, $off, SEEK_SET); + fwrite($fp, PackUtil::packLongLE($size)); + $off += 8; + } + // Write out Compressed Size. + $compressedSize = $this->getCompressedSize(); + if (0xffffffff <= $compressedSize) { + fseek($fp, $off, SEEK_SET); + fwrite($fp, PackUtil::packLongLE($compressedSize)); + $off += 8; + } + // Write out Relative Header Offset. + $offset = $this->getOffset(); + if (0xffffffff <= $offset) { + fseek($fp, $off, SEEK_SET); + fwrite($fp, PackUtil::packLongLE($offset)); + $off += 8; + } + // Create ZIP64 Extended Information Extra Field from serialized data. + $field = null; + if ($off > 0) { + $field = new DefaultExtraField(ExtraField::ZIP64_HEADER_ID); + $field->readFrom($fp, 0, $off); + } else { + $field = null; + } + return $field; + } + + /** + * 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 + * or do not conform to the ZIP File Format Specification + */ + public function setExtra($data) + { + if (null !== $data) { + $length = strlen($data); + if (0x0000 > $length || $length > 0xffff) { + throw new ZipException("Extra Fields too large"); + } + } + if (null === $data || strlen($data) <= 0) { + $this->fields = null; + } else { + $this->setExtraFields($data, false); + } + } + + /** + * @param string $data + * @param bool $zip64 + */ + private function setExtraFields($data, $zip64) + { + if (null === $this->fields) { + $this->fields = new ExtraFields(); + } + $fp = fopen('php://temp', 'r+b'); + fwrite($fp, $data); + rewind($fp); + $this->fields->readFrom($fp, 0, strlen($data)); + $result = false; + if ($zip64) { + $result = $this->parseZip64ExtraField(); + } + if ($result) { + $this->fields->remove(ExtraField::ZIP64_HEADER_ID); + if ($this->fields->size() <= 0) { + if (0 !== $this->fields->size()) { + $this->fields = null; + } + } + } + fclose($fp); + } + + /** + * Parses the properties of this entry from the ZIP64 Extended Information + * Extra Field, if present. + * The ZIP64 Extended Information Extra Field is not removed. + * + * @return bool + * @throws ZipException + */ + private function parseZip64ExtraField() + { + if (null === $this->fields) { + return false; + } + $ef = $this->fields->get(ExtraField::ZIP64_HEADER_ID); + if (null === $ef) { + return false; + } + $handle = $ef->getDataBlock(); + $off = 0; + // Read in Uncompressed Size. + $size = $this->getRawSize(); + if (0xffffffff <= $size) { + assert(0xffffffff === $size); + fseek($handle, $off, SEEK_SET); + $this->setRawSize(PackUtil::unpackLongLE(fread($handle, 8))); + $off += 8; + } + // Read in Compressed Size. + $compressedSize = $this->getRawCompressedSize(); + if (0xffffffff <= $compressedSize) { + assert(0xffffffff === $compressedSize); + fseek($handle, $off, SEEK_SET); + $this->setRawCompressedSize(PackUtil::unpackLongLE(fread($handle, 8))); + $off += 8; + } + // Read in Relative Header Offset. + $offset = $this->getRawOffset(); + if (0xffffffff <= $offset) { + assert(0xffffffff, $offset); + fseek($handle, $off, SEEK_SET); + $this->setRawOffset(PackUtil::unpackLongLE(fread($handle, 8))); + //$off += 8; + } + fclose($handle); + return true; + } + + /** + * @return int + */ + public function getRawSize() + { + $size = $this->size; + if (self::UNKNOWN == $size) return 0; + return 0xffffffff <= $size ? 0xffffffff : $size; + } + + /** + * @param int $size + * @throws ZipException + */ + public function setRawSize($size) + { + if (0 > $size || $size > 0x7fffffffffffffff) { + throw new ZipException("Uncompressed Size out of range - " . $this->name); + } + $this->size = $size; + } + + /** + * @return int + */ + public function getRawCompressedSize() + { + $compressedSize = $this->compressedSize; + if (self::UNKNOWN == $compressedSize) return 0; + return 0xffffffff <= $compressedSize + ? 0xffffffff + : $compressedSize; + } + + /** + * @param int $compressedSize + * @throws ZipException + */ + public function setRawCompressedSize($compressedSize) + { + if (0 > $compressedSize || $compressedSize > 0x7fffffffffffffff) { + throw new ZipException("Compressed size out of range - " . $this->name); + } + $this->compressedSize = $compressedSize; + } + + /** + * @return int + */ + public function getRawOffset() + { + $offset = $this->offset; + if (self::UNKNOWN == $offset) return 0; + return 0xffffffff <= $offset ? 0xffffffff : $offset; + } + + /** + * Set relative Offset Of Local File Header. + * + * @param int $offset + * @throws ZipException + */ + public function setRawOffset($offset) + { + if (0 > $offset || $offset > 0x7fffffffffffffff) { + throw new ZipException("Offset out of range - " . $this->name); + } + $this->offset = $offset; + } + + /** + * Returns a protective copy of the serialized Extra Fields. + * + * @return string A new byte array holding the serialized Extra Fields. + * null is never returned. + * @see ZipEntry::getRawExtraFields() + */ + public function getRawExtraFields() + { + return $this->getExtraFields(true); + } + + /** + * Sets extra fields and parses ZIP64 extra field. + * This method must not get called before the uncompressed size, + * compressed size and offset have been initialized! + * + * @param string $data + * @throws ZipException + */ + public function setRawExtraFields($data) + { + $length = strlen($data); + if (0 < $length && (0x0000 > $length || $length > 0xffff)) { + throw new ZipException("Extra Fields too large"); + } + $this->setExtraFields($data, true); + } + + /** + * Returns comment entry + * + * @return string + */ + public function getComment() + { + return $this->comment; + } + + /** + * Sets the entry comment. + * Note that this method limits the comment size to 64 KB. + * Therefore, this property should not be used to hold arbitrary + * (application) data. + * Consider storing such data in a separate entry instead. + * + * @param string $comment The entry comment. + * @throws ZipException + */ + public function setComment($comment) + { + if (null !== $comment) { + $commentLength = strlen($comment); + if (0x0000 > $commentLength || $commentLength > 0xffff) { + throw new ZipException("Comment too long"); + } + } + $encoding = mb_detect_encoding($this->name, "ASCII, UTF-8", true); + if ($encoding === 'UTF-8') { + $this->setGeneralPurposeBitFlag(self::GPBF_UTF8, true); + } + $this->comment = $comment; + } + + /** + * @return string + */ + public function getRawComment() + { + return null != $this->comment ? $this->comment : ""; + } + + /** + * @param string $comment + * @throws ZipException + */ + public function setRawComment($comment) + { + $commentLength = strlen($comment); + if (0x0000 > $commentLength || $commentLength > 0xffff) { + throw new ZipException("Comment too long"); + } + $this->comment = $comment; + } + + /** + * @return bool + */ + public function isDataDescriptorRequired() + { + return self::UNKNOWN == ($this->getCrc() | $this->getCompressedSize() | $this->getSize()); + } + + /** + * Return crc32 content or 0 for WinZip AES v2 + * + * @return int + */ + public function getCrc() + { + return $this->isInit(self::BIT_CRC) ? $this->crc & 0xffffffff : self::UNKNOWN; + } + + /** + * Set crc32 content. + * + * @param int $crc + * @throws ZipException + */ + public function setCrc($crc) + { + $known = self::UNKNOWN != $crc; + if ($known) { + if (0x00000000 > $crc || $crc > 0xffffffff) { + throw new ZipException("CRC-32 out of range - " . $this->name); + } + $this->crc = $crc; + } else { + $this->crc = 0; + } + $this->setInit(self::BIT_CRC, $known); + } + + /** + * @return string + */ + public function getPassword() + { + return $this->password; + } + + /** + * Set password and encryption method from entry + * + * @param string $password + * @param null|int $encryptionMethod + */ + public function setPassword($password, $encryptionMethod = null) + { + $this->password = $password; + if ($encryptionMethod !== null) { + $this->setEncryptionMethod($encryptionMethod); + } + $this->setEncrypted(!empty($this->password)); + } + + /** + * @return int + */ + public function getEncryptionMethod() + { + return $this->encryptionMethod; + } + + /** + * Set encryption method + * + * @see ZipEntry::ENCRYPTION_METHOD_TRADITIONAL + * @see ZipEntry::ENCRYPTION_METHOD_WINZIP_AES + * + * @param int $encryptionMethod + * @throws ZipException + */ + public function setEncryptionMethod($encryptionMethod) + { + if ( + self::ENCRYPTION_METHOD_TRADITIONAL !== $encryptionMethod && + self::ENCRYPTION_METHOD_WINZIP_AES !== $encryptionMethod + ) { + throw new ZipException('Invalid encryption method'); + } + $this->encryptionMethod = $encryptionMethod; + $this->setEncrypted(true); + } + +} \ No newline at end of file diff --git a/src/PhpZip/Model/ZipInfo.php b/src/PhpZip/Model/ZipInfo.php new file mode 100644 index 0000000..2ef9be2 --- /dev/null +++ b/src/PhpZip/Model/ZipInfo.php @@ -0,0 +1,384 @@ + '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 $valuesCompressionMethod = [ + ZipEntry::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', + ZipEntry::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::WINZIP_AES => 'WinZip AES', + ]; + + /** + * @var string + */ + private $path; + + /** + * @var bool + */ + private $folder; + + /** + * @var int + */ + private $size; + + /** + * @var int + */ + private $compressedSize; + + /** + * @var int + */ + private $mtime; + + /** + * @var int|null + */ + private $ctime; + + /** + * @var int|null + */ + private $atime; + + /** + * @var bool + */ + private $encrypted; + + /** + * @var string|null + */ + private $comment; + + /** + * @var int + */ + private $crc; + + /** + * @var string + */ + private $method; + + /** + * @var string + */ + private $platform; + + /** + * @var int + */ + private $version; + + /** + * ZipInfo constructor. + * + * @param ZipEntry $entry + */ + public function __construct(ZipEntry $entry) + { + $mtime = $entry->getTime(); + $atime = null; + $ctime = null; + + $field = $entry->getExtraField(NtfsExtraField::getHeaderId()); + if ($field !== null && $field instanceof NtfsExtraField) { + /** + * @var NtfsExtraField $field + */ + $atime = $field->getAtime(); + $ctime = $field->getCtime(); + } + + $this->path = $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->comment = $entry->getComment(); + $this->crc = $entry->getCrc(); + $this->method = self::getMethodName($entry); + $this->platform = self::getPlatformName($entry); + $this->version = $entry->getVersionNeededToExtract(); + } + + /** + * @param ZipEntry $entry + * @return string + */ + public static function getMethodName(ZipEntry $entry) + { + $return = ''; + if ($entry->isEncrypted()) { + if ($entry->getMethod() === ZipEntry::WINZIP_AES) { + $field = $entry->getExtraField(WinZipAesEntryExtraField::getHeaderId()); + $return = ucfirst(self::$valuesCompressionMethod[$entry->getMethod()]); + if ($field !== null) { + /** + * @var WinZipAesEntryExtraField $field + */ + $return .= '-' . $field->getKeyStrength(); + if (isset(self::$valuesCompressionMethod[$field->getMethod()])) { + $return .= ' ' . ucfirst(self::$valuesCompressionMethod[$field->getMethod()]); + } + } + } else { + $return .= 'ZipCrypto'; + if (isset(self::$valuesCompressionMethod[$entry->getMethod()])) { + $return .= ' ' . ucfirst(self::$valuesCompressionMethod[$entry->getMethod()]); + } + } + } elseif (isset(self::$valuesCompressionMethod[$entry->getMethod()])) { + $return = ucfirst(self::$valuesCompressionMethod[$entry->getMethod()]); + } else { + $return = 'unknown'; + } + return $return; + } + + /** + * @param ZipEntry $entry + * @return string + */ + public static function getPlatformName(ZipEntry $entry) + { + if (isset(self::$valuesMadeBy[$entry->getPlatform()])) { + return self::$valuesMadeBy[$entry->getPlatform()]; + } else { + return 'unknown'; + } + } + + /** + * @return array + */ + public function toArray() + { + return [ + 'path' => $this->getPath(), + 'folder' => $this->isFolder(), + 'size' => $this->getSize(), + 'compressed_size' => $this->getCompressedSize(), + 'modified' => $this->getMtime(), + 'created' => $this->getCtime(), + 'accessed' => $this->getAtime(), + 'encrypted' => $this->isEncrypted(), + 'comment' => $this->getComment(), + 'crc' => $this->getCrc(), + 'method' => $this->getMethod(), + 'platform' => $this->getPlatform(), + 'version' => $this->getVersion() + ]; + } + + /** + * @return string + */ + public function getPath() + { + return $this->path; + } + + /** + * @return boolean + */ + 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 boolean + */ + public function isEncrypted() + { + return $this->encrypted; + } + + /** + * @return null|string + */ + public function getComment() + { + return $this->comment; + } + + /** + * @return int + */ + public function getCrc() + { + return $this->crc; + } + + /** + * @return string + */ + public function getMethod() + { + return $this->method; + } + + /** + * @return string + */ + public function getPlatform() + { + return $this->platform; + } + + /** + * @return int + */ + public function getVersion() + { + return $this->version; + } + + /** + * @return string + */ + function __toString() + { + return 'ZipInfo {' + . 'Path="' . $this->getPath() . '", ' + . ($this->isFolder() ? 'Folder, ' : '') + . 'Size=' . FilesUtil::humanSize($this->getSize()) + . ', Compressed size=' . FilesUtil::humanSize($this->getCompressedSize()) + . ', Modified time=' . date(DATE_W3C, $this->getMtime()) . ', ' + . ($this->getCtime() !== null ? 'Created time=' . date(DATE_W3C, $this->getCtime()) . ', ' : '') + . ($this->getAtime() !== null ? 'Accessed time=' . date(DATE_W3C, $this->getAtime()) . ', ' : '') + . ($this->isEncrypted() ? 'Encrypted, ' : '') + . (!empty($this->comment) ? 'Comment="' . $this->getComment() . '", ' : '') + . (!empty($this->crc) ? 'Crc=0x' . dechex($this->getCrc()) . ', ' : '') + . 'Method="' . $this->getMethod() . '", ' + . 'Platform="' . $this->getPlatform() . '", ' + . 'Version=' . $this->getVersion() + . '}'; + } + + +} \ No newline at end of file diff --git a/src/PhpZip/Output/ZipOutputEmptyDirEntry.php b/src/PhpZip/Output/ZipOutputEmptyDirEntry.php new file mode 100644 index 0000000..075e16a --- /dev/null +++ b/src/PhpZip/Output/ZipOutputEmptyDirEntry.php @@ -0,0 +1,22 @@ +entry = $entry; + } + + /** + * Returns zip entry + * + * @return ZipEntry + */ + public function getEntry() + { + return $this->entry; + } + + /** + * Returns entry data. + * + * @return string + */ + abstract public function getEntryContent(); +} \ No newline at end of file diff --git a/src/PhpZip/Output/ZipOutputStreamEntry.php b/src/PhpZip/Output/ZipOutputStreamEntry.php new file mode 100644 index 0000000..2f6b701 --- /dev/null +++ b/src/PhpZip/Output/ZipOutputStreamEntry.php @@ -0,0 +1,54 @@ +stream = $stream; + } + + /** + * Returns entry data. + * + * @return string + */ + public function getEntryContent() + { + rewind($this->stream); + return stream_get_contents($this->stream); + } + + /** + * Release stream resource. + */ + function __destruct() + { + if ($this->stream !== null) { + fclose($this->stream); + $this->stream = null; + } + } +} \ No newline at end of file diff --git a/src/PhpZip/Output/ZipOutputStringEntry.php b/src/PhpZip/Output/ZipOutputStringEntry.php new file mode 100644 index 0000000..508e56a --- /dev/null +++ b/src/PhpZip/Output/ZipOutputStringEntry.php @@ -0,0 +1,46 @@ +data = $data; + } + + /** + * Returns entry data. + * + * @return string + */ + public function getEntryContent() + { + return $this->data; + } +} \ No newline at end of file diff --git a/src/PhpZip/Output/ZipOutputZipFileEntry.php b/src/PhpZip/Output/ZipOutputZipFileEntry.php new file mode 100644 index 0000000..418ac20 --- /dev/null +++ b/src/PhpZip/Output/ZipOutputZipFileEntry.php @@ -0,0 +1,56 @@ +inputZipFile = $zipFile; + $this->inputEntryName = $zipEntry->getName(); + } + + /** + * Returns entry data. + * + * @return string + */ + public function getEntryContent() + { + return $this->inputZipFile->getEntryContent($this->inputEntryName); + } +} \ No newline at end of file diff --git a/src/PhpZip/Util/CryptoUtil.php b/src/PhpZip/Util/CryptoUtil.php new file mode 100644 index 0000000..285e766 --- /dev/null +++ b/src/PhpZip/Util/CryptoUtil.php @@ -0,0 +1,32 @@ +> 1); + + /** + * Convert a 32 bit integer DOS date/time value to a UNIX timestamp value. + * + * @param int $dosTime Dos date/time + * @return int Unix timestamp + */ + public static function toUnixTimestamp($dosTime) + { + if (self::MIN_DOS_TIME > $dosTime) { + $dosTime = self::MIN_DOS_TIME; + } elseif (self::MAX_DOS_TIME < $dosTime) { + $dosTime = self::MAX_DOS_TIME; + } + + return mktime( + ($dosTime >> 11) & 0x1f, // hour + ($dosTime >> 5) & 0x3f, // minute + 2 * ($dosTime & 0x1f), // second + ($dosTime >> 21) & 0x0f, // month + ($dosTime >> 16) & 0x1f, // day + 1980 + (($dosTime >> 25) & 0x7f) // year + ); + } + + /** + * Converts a UNIX timestamp value to a DOS date/time value. + * + * @param int $unixTimestamp The number of seconds since midnight, January 1st, + * 1970 AD UTC. + * @return int A DOS date/time value reflecting the local time zone and + * rounded down to even seconds + * and is in between DateTimeConverter::MIN_DOS_TIME and DateTimeConverter::MAX_DOS_TIME. + * @throws ZipException If unix timestamp is negative. + */ + public static function toDosTime($unixTimestamp) + { + if (0 > $unixTimestamp) { + throw new ZipException("Negative unix timestamp: " . $unixTimestamp); + } + + $date = getdate($unixTimestamp); + + if ($date['year'] < 1980) { + return self::MIN_DOS_TIME; + } + + $date['year'] -= 1980; + return ($date['year'] << 25 | $date['mon'] << 21 | + $date['mday'] << 16 | $date['hours'] << 11 | + $date['minutes'] << 5 | $date['seconds'] >> 1); + } +} \ No newline at end of file diff --git a/src/PhpZip/Util/FilesUtil.php b/src/PhpZip/Util/FilesUtil.php new file mode 100644 index 0000000..9192c6d --- /dev/null +++ b/src/PhpZip/Util/FilesUtil.php @@ -0,0 +1,222 @@ +isDir() ? 'rmdir' : 'unlink'); + $function($fileInfo->getRealPath()); + } + rmdir($dir); + } + + + /** + * Convert glob pattern to regex pattern. + * + * @param string $globPattern + * @return string + */ + public static function convertGlobToRegEx($globPattern) + { + // Remove beginning and ending * globs because they're useless + $globPattern = trim($globPattern, '*'); + $escaping = false; + $inCurrent = 0; + $chars = str_split($globPattern); + $regexPattern = ''; + foreach ($chars AS $currentChar) { + switch ($currentChar) { + case '*': + $regexPattern .= ($escaping ? "\\*" : '.*'); + $escaping = false; + break; + case '?': + $regexPattern .= ($escaping ? "\\?" : '.'); + $escaping = false; + break; + case '.': + case '(': + case ')': + case '+': + case '|': + case '^': + case '$': + case '@': + case '%': + $regexPattern .= '\\' . $currentChar; + $escaping = false; + break; + case '\\': + if ($escaping) { + $regexPattern .= "\\\\"; + $escaping = false; + } else { + $escaping = true; + } + break; + case '{': + if ($escaping) { + $regexPattern .= "\\{"; + } else { + $regexPattern = '('; + $inCurrent++; + } + $escaping = false; + break; + case '}': + if ($inCurrent > 0 && !$escaping) { + $regexPattern .= ')'; + $inCurrent--; + } else if ($escaping) + $regexPattern = "\\}"; + else + $regexPattern = "}"; + $escaping = false; + break; + case ',': + if ($inCurrent > 0 && !$escaping) { + $regexPattern .= '|'; + } else if ($escaping) + $regexPattern .= "\\,"; + else + $regexPattern = ","; + break; + default: + $escaping = false; + $regexPattern .= $currentChar; + } + } + return $regexPattern; + } + + /** + * Search files. + * + * @param string $inputDir + * @param bool $recursive + * @param array $ignoreFiles + * @return array Searched file list + */ + public static function fileSearchWithIgnore($inputDir, $recursive = true, array $ignoreFiles = []) + { + $directoryIterator = $recursive ? + new \RecursiveDirectoryIterator($inputDir) : + new \DirectoryIterator($inputDir); + + if (!empty($ignoreFiles)) { + $directoryIterator = $recursive ? + new IgnoreFilesRecursiveFilterIterator($directoryIterator, $ignoreFiles) : + new IgnoreFilesFilterIterator($directoryIterator, $ignoreFiles); + } + + $iterator = $recursive ? + new \RecursiveIteratorIterator($directoryIterator) : + new \IteratorIterator($directoryIterator); + + $fileList = []; + foreach ($iterator as $file) { + if ($file instanceof \SplFileInfo) { + $fileList[] = $file->getPathname(); + } + } + return $fileList; + } + + /** + * Search files from glob pattern. + * + * @param string $globPattern + * @param int $flags + * @param bool $recursive + * @return array Searched file list + */ + public static function globFileSearch($globPattern, $flags = 0, $recursive = true) + { + $flags = (int)$flags; + $recursive = (bool)$recursive; + $files = glob($globPattern, $flags); + if (!$recursive) { + return $files; + } + foreach (glob(dirname($globPattern) . '/*', GLOB_ONLYDIR | GLOB_NOSORT) as $dir) { + $files = array_merge($files, self::globFileSearch($dir . '/' . basename($globPattern), $flags, $recursive)); + } + return $files; + } + + /** + * Search files from regex pattern. + * + * @param string $folder + * @param string $pattern + * @param bool $recursive + * @return array Searched file list + */ + public static function regexFileSearch($folder, $pattern, $recursive = true) + { + $directoryIterator = $recursive ? new \RecursiveDirectoryIterator($folder) : new \DirectoryIterator($folder); + $iterator = $recursive ? new \RecursiveIteratorIterator($directoryIterator) : new \IteratorIterator($directoryIterator); + $regexIterator = new \RegexIterator($iterator, $pattern, \RegexIterator::MATCH); + $fileList = []; + foreach ($regexIterator as $file) { + if ($file instanceof \SplFileInfo) { + $fileList[] = $file->getPathname(); + } + } + return $fileList; + } + + /** + * Convert bytes to human size. + * + * @param int $size Size bytes + * @param string|null $unit Unit support 'GB', 'MB', 'KB' + * @return string + */ + public static function humanSize($size, $unit = null) + { + if (($unit === null && $size >= 1 << 30) || $unit === "GB") + return number_format($size / (1 << 30), 2) . "GB"; + if (($unit === null && $size >= 1 << 20) || $unit === "MB") + return number_format($size / (1 << 20), 2) . "MB"; + if (($unit === null && $size >= 1 << 10) || $unit === "KB") + return number_format($size / (1 << 10), 2) . "KB"; + return number_format($size) . " bytes"; + } +} \ No newline at end of file diff --git a/src/PhpZip/Util/Iterator/IgnoreFilesFilterIterator.php b/src/PhpZip/Util/Iterator/IgnoreFilesFilterIterator.php new file mode 100644 index 0000000..a3b9c9d --- /dev/null +++ b/src/PhpZip/Util/Iterator/IgnoreFilesFilterIterator.php @@ -0,0 +1,60 @@ +ignoreFiles = array_merge($this->ignoreFiles, $ignoreFiles); + } + + /** + * Check whether the current element of the iterator is acceptable + * @link http://php.net/manual/en/filteriterator.accept.php + * @return bool true if the current element is acceptable, otherwise false. + * @since 5.1.0 + */ + public function accept() + { + /** + * @var \SplFileInfo $fileInfo + */ + $fileInfo = $this->current(); + $pathname = str_replace('\\', '/', $fileInfo->getPathname()); + foreach ($this->ignoreFiles as $ignoreFile) { + // handler dir and sub dir + if ($fileInfo->isDir() + && $ignoreFile[strlen($ignoreFile) - 1] === '/' + && StringUtil::endsWith($pathname, substr($ignoreFile, 0, -1)) + ) { + return false; + } + + // handler filename + if (StringUtil::endsWith($pathname, $ignoreFile)) { + return false; + } + } + return true; + } +} \ No newline at end of file diff --git a/src/PhpZip/Util/Iterator/IgnoreFilesRecursiveFilterIterator.php b/src/PhpZip/Util/Iterator/IgnoreFilesRecursiveFilterIterator.php new file mode 100644 index 0000000..131ee3f --- /dev/null +++ b/src/PhpZip/Util/Iterator/IgnoreFilesRecursiveFilterIterator.php @@ -0,0 +1,69 @@ +ignoreFiles = array_merge($this->ignoreFiles, $ignoreFiles); + } + + /** + * Check whether the current element of the iterator is acceptable + * @link http://php.net/manual/en/filteriterator.accept.php + * @return bool true if the current element is acceptable, otherwise false. + * @since 5.1.0 + */ + public function accept() + { + /** + * @var \SplFileInfo $fileInfo + */ + $fileInfo = $this->current(); + $pathname = str_replace('\\', '/', $fileInfo->getPathname()); + foreach ($this->ignoreFiles as $ignoreFile) { + // handler dir and sub dir + if ($fileInfo->isDir() + && $ignoreFile[strlen($ignoreFile) - 1] === '/' + && StringUtil::endsWith($pathname, substr($ignoreFile, 0, -1)) + ) { + return false; + } + + // handler filename + if (StringUtil::endsWith($pathname, $ignoreFile)) { + return false; + } + } + return true; + } + + /** + * @return IgnoreFilesRecursiveFilterIterator + */ + public function getChildren() + { + return new self($this->getInnerIterator()->getChildren(), $this->ignoreFiles); + } +} \ No newline at end of file diff --git a/src/PhpZip/Util/PackUtil.php b/src/PhpZip/Util/PackUtil.php new file mode 100644 index 0000000..a7f54a9 --- /dev/null +++ b/src/PhpZip/Util/PackUtil.php @@ -0,0 +1,44 @@ += 0) {return pack("P", $longValue);} + + $left = 0xffffffff00000000; + $right = 0x00000000ffffffff; + + $r = ($longValue & $left) >> 32; + $l = $longValue & $right; + + return pack('VV', $l, $r); + } + + /** + * @param string|int $value + * @return int + * @throws ZipException + */ + public static function unpackLongLE($value) + { + // TODO test if (version_compare(PHP_VERSION, '5.6.3') >= 0){ return current(unpack('P', $value)); } + $unpack = unpack('Va/Vb', $value); + return $unpack['a'] + ($unpack['b'] << 32); + } + +} \ No newline at end of file diff --git a/src/PhpZip/Util/StringUtil.php b/src/PhpZip/Util/StringUtil.php new file mode 100644 index 0000000..c596adf --- /dev/null +++ b/src/PhpZip/Util/StringUtil.php @@ -0,0 +1,30 @@ += 0 + && strpos($haystack, $needle, $temp) !== false); + } +} \ No newline at end of file diff --git a/src/PhpZip/ZipConstants.php b/src/PhpZip/ZipConstants.php new file mode 100644 index 0000000..5082cba --- /dev/null +++ b/src/PhpZip/ZipConstants.php @@ -0,0 +1,115 @@ +mapper = new PositionMapper(); + $this->charset = "UTF-8"; + } + + /** + * Open zip archive from file + * + * @param string $filename + * @return ZipFile + * @throws IllegalArgumentException if file doesn't exists. + * @throws ZipException if can't open file. + */ + public static function openFromFile($filename) + { + if (!file_exists($filename)) { + throw new IllegalArgumentException("File $filename can't exists."); + } + if (!($handle = fopen($filename, 'rb'))) { + throw new ZipException("File $filename can't open."); + } + $zipFile = self::openFromStream($handle); + $zipFile->length = filesize($filename); + return $zipFile; + } + + /** + * Open zip archive from stream resource + * + * @param resource $handle + * @return ZipFile + * @throws IllegalArgumentException Invalid stream resource + * or resource cannot seekable stream + */ + public static function openFromStream($handle) + { + if (!is_resource($handle)) { + throw new IllegalArgumentException("Invalid stream resource."); + } + $meta = stream_get_meta_data($handle); + if (!$meta['seekable']) { + throw new IllegalArgumentException("Resource cannot seekable stream."); + } + $zipFile = new self(); + $stats = fstat($handle); + if (isset($stats['size'])) { + $zipFile->length = $stats['size']; + } + $zipFile->checkZipFileSignature($handle); + $numEntries = $zipFile->findCentralDirectory($handle); + $zipFile->mountCentralDirectory($handle, $numEntries); + if ($zipFile->preamble + $zipFile->postamble >= $zipFile->length) { + assert(0 === $numEntries); + $zipFile->checkZipFileSignature($handle); + } + assert(null !== $handle); + assert(null !== $zipFile->charset); + assert(null !== $zipFile->entries); + assert(null !== $zipFile->mapper); + $zipFile->inputStream = $handle; + // Do NOT close stream! + return $zipFile; + } + + /** + * Check zip file signature + * + * @param resource $handle + * @throws ZipException if this not .ZIP file. + */ + private function checkZipFileSignature($handle) + { + rewind($handle); + $signature = current(unpack('V', fread($handle, 4))); + // Constraint: A ZIP file must start with a Local File Header + // or a (ZIP64) End Of Central Directory Record if it's empty. + if (self::LOCAL_FILE_HEADER_SIG !== $signature && self::ZIP64_END_OF_CENTRAL_DIRECTORY_RECORD_SIG !== $signature && self::END_OF_CENTRAL_DIRECTORY_RECORD_SIG !== $signature + ) { + throw new ZipException("Expected Local File Header or (ZIP64) End Of Central Directory Record! Signature: " . $signature); + } + } + + /** + * Positions the file pointer at the first Central File Header. + * Performs some means to check that this is really a ZIP file. + * + * @param resource $handle + * @return int + * @throws ZipException If the file is not compatible to the ZIP File + * Format Specification. + */ + private function findCentralDirectory($handle) + { + // Search for End of central directory record. + $max = $this->length - self::END_OF_CENTRAL_DIRECTORY_RECORD_MIN_LEN; + $min = $max >= 0xffff ? $max - 0xffff : 0; + for ($endOfCentralDirRecordPos = $max; $endOfCentralDirRecordPos >= $min; $endOfCentralDirRecordPos--) { + fseek($handle, $endOfCentralDirRecordPos, SEEK_SET); + // end of central dir signature 4 bytes (0x06054b50) + if (self::END_OF_CENTRAL_DIRECTORY_RECORD_SIG !== current(unpack('V', fread($handle, 4)))) + continue; + + // Process End Of Central Directory Record. + $data = fread($handle, self::END_OF_CENTRAL_DIRECTORY_RECORD_MIN_LEN - 4); + + /** + * @var int $diskNo number of this disk - 2 bytes + * @var int $cdDiskNo number of the disk with the start of the + * central directory - 2 bytes + * @var int $cdEntriesDisk total number of entries in the central + * directory on this disk - 2 bytes + * @var int $cdEntries total number of entries in the central + * directory - 2 bytes + * @var int $cdSize size of the central directory - 4 bytes + * @var int $cdPos offset of start of central directory with + * respect to the starting disk number - 4 bytes + * @var int $commentLen ZIP file comment length - 2 bytes + */ + $unpack = unpack('vdiskNo/vcdDiskNo/vcdEntriesDisk/vcdEntries/VcdSize/VcdPos/vcommentLen', $data); + extract($unpack); + + if (0 !== $diskNo || 0 !== $cdDiskNo || $cdEntriesDisk !== $cdEntries) { + throw new ZipException( + "ZIP file spanning/splitting is not supported!" + ); + } + // .ZIP file comment (variable size) + if (0 < $commentLen) { + $this->comment = fread($handle, $commentLen); + } + $this->preamble = $endOfCentralDirRecordPos; + $this->postamble = $this->length - ftell($handle); + + // Check for ZIP64 End Of Central Directory Locator. + $endOfCentralDirLocatorPos = $endOfCentralDirRecordPos - self::ZIP64_END_OF_CENTRAL_DIRECTORY_LOCATOR_LEN; + + fseek($handle, $endOfCentralDirLocatorPos, SEEK_SET); + + // zip64 end of central dir locator + // signature 4 bytes (0x07064b50) + if ( + 0 > $endOfCentralDirLocatorPos || + ftell($handle) === $this->length || + self::ZIP64_END_OF_CENTRAL_DIRECTORY_LOCATOR_SIG !== current(unpack('V', fread($handle, 4))) + ) { + // Seek and check first CFH, probably requiring an offset mapper. + $offset = $endOfCentralDirRecordPos - $cdSize; + fseek($handle, $offset, SEEK_SET); + $offset -= $cdPos; + if (0 !== $offset) { + $this->mapper = new OffsetPositionMapper($offset); + } + return (int)$cdEntries; + } + + // number of the disk with the + // start of the zip64 end of + // central directory 4 bytes + $zip64EndOfCentralDirectoryRecordDisk = current(unpack('V', fread($handle, 4))); + // relative offset of the zip64 + // end of central directory record 8 bytes + $zip64EndOfCentralDirectoryRecordPos = PackUtil::unpackLongLE(fread($handle, 8)); + // total number of disks 4 bytes + $totalDisks = current(unpack('V', fread($handle, 4))); + if (0 !== $zip64EndOfCentralDirectoryRecordDisk || 1 !== $totalDisks) { + throw new ZipException("ZIP file spanning/splitting is not supported!"); + } + fseek($handle, $zip64EndOfCentralDirectoryRecordPos, SEEK_SET); + // zip64 end of central dir + // signature 4 bytes (0x06064b50) + $zip64EndOfCentralDirSig = current(unpack('V', fread($handle, 4))); + if (self::ZIP64_END_OF_CENTRAL_DIRECTORY_RECORD_SIG !== $zip64EndOfCentralDirSig) { + throw new ZipException("Expected ZIP64 End Of Central Directory Record!"); + } + // size of zip64 end of central + // directory record 8 bytes + // version made by 2 bytes + // version needed to extract 2 bytes + fseek($handle, 12, SEEK_CUR); + // number of this disk 4 bytes + $diskNo = current(unpack('V', fread($handle, 4))); + // number of the disk with the + // start of the central directory 4 bytes + $cdDiskNo = current(unpack('V', fread($handle, 4))); + // total number of entries in the + // central directory on this disk 8 bytes + $cdEntriesDisk = PackUtil::unpackLongLE(fread($handle, 8)); + // total number of entries in the + // central directory 8 bytes + $cdEntries = PackUtil::unpackLongLE(fread($handle, 8)); + if (0 !== $diskNo || 0 !== $cdDiskNo || $cdEntriesDisk !== $cdEntries) { + throw new ZipException( + "ZIP file spanning/splitting is not supported!"); + } + if ($cdEntries < 0 || 0x7fffffff < $cdEntries) { + throw new ZipException( + "Total Number Of Entries In The Central Directory out of range!"); + } + // size of the central directory 8 bytes + //$cdSize = self::getLongLE($channel); + fseek($handle, 8, SEEK_CUR); + // offset of start of central + // directory with respect to + // the starting disk number 8 bytes + $cdPos = PackUtil::unpackLongLE(fread($handle, 8)); + // zip64 extensible data sector (variable size) + fseek($handle, $cdPos, SEEK_SET); + $this->preamble = $zip64EndOfCentralDirectoryRecordPos; + return (int)$cdEntries; + } + // Start recovering file entries from min. + $this->preamble = $min; + $this->postamble = $this->length - $min; + return 0; + } + + /** + * Reads the central directory from the given seekable byte channel + * and populates the internal tables with ZipEntry instances. + * + * The ZipEntry's will know all data that can be obtained from the + * central directory alone, but not the data that requires the local + * file header or additional data to be read. + * + * @param resource $handle Input channel. + * @param int $numEntries Size zip entries. + * @throws ZipException + */ + private function mountCentralDirectory($handle, $numEntries) + { + $numEntries = (int)$numEntries; + $entries = []; + for (; ; $numEntries--) { + // central file header signature 4 bytes (0x02014b50) + if (self::CENTRAL_FILE_HEADER_SIG !== current(unpack('V', fread($handle, 4)))) { + break; + } + // version made by 2 bytes + $versionMadeBy = current(unpack('v', fread($handle, 2))); + + // version needed to extract 2 bytes + fseek($handle, 2, SEEK_CUR); + + $unpack = unpack('vgpbf/vrawMethod/VrawTime/VrawCrc/VrawCompressedSize/VrawSize/vfileLen/vextraLen/vcommentLen', fread($handle, 26)); + + // disk number start 2 bytes + // internal file attributes 2 bytes + fseek($handle, 4, SEEK_CUR); + + // external file attributes 4 bytes + // relative offset of local header 4 bytes + $unpack2 = unpack('VrawExternalAttributes/VlfhOff', fread($handle, 8)); + + $utf8 = 0 !== ($unpack['gpbf'] & ZipEntry::GPBF_UTF8); + if ($utf8) { + $this->charset = "UTF-8"; + } + + // See appendix D of PKWARE's ZIP File Format Specification. + $name = fread($handle, $unpack['fileLen']); + $entry = new ZipEntry($name, $handle); + $entry->setRawPlatform($versionMadeBy >> 8); + $entry->setGeneralPurposeBitFlags($unpack['gpbf']); + $entry->setRawMethod($unpack['rawMethod']); + $entry->setRawTime($unpack['rawTime']); + $entry->setRawCrc($unpack['rawCrc']); + $entry->setRawCompressedSize($unpack['rawCompressedSize']); + $entry->setRawSize($unpack['rawSize']); + $entry->setRawExternalAttributes($unpack2['rawExternalAttributes']); + $entry->setRawOffset($unpack2['lfhOff']); // must be unmapped! + if (0 < $unpack['extraLen']) { + $entry->setRawExtraFields(fread($handle, $unpack['extraLen'])); + } + if (0 < $unpack['commentLen']) { + $entry->setComment(fread($handle, $unpack['commentLen'])); + } + + unset($unpack, $unpack2); + + // Re-load virtual offset after ZIP64 Extended Information + // Extra Field may have been parsed, map it to the real + // offset and conditionally update the preamble size from it. + $lfhOff = $this->mapper->map($entry->getOffset()); + if ($lfhOff < $this->preamble) { + $this->preamble = $lfhOff; + } + $entries[$entry->getName()] = $entry; + } + + if (0 !== $numEntries % 0x10000) { + throw new ZipException("Expected " . abs($numEntries) . + ($numEntries > 0 ? " more" : " less") . + " entries in the Central Directory!"); + } + + $this->entries = $entries; + } + + /** + * Open zip archive from raw string data. + * + * @param string $data + * @return ZipFile + * @throws IllegalArgumentException if data not available. + * @throws ZipException if can't open temp stream. + */ + public static function openFromString($data) + { + if (empty($data)) { + throw new IllegalArgumentException("Data not available"); + } + if (!($handle = fopen('php://temp', 'r+b'))) { + throw new ZipException("Can't open temp stream."); + } + fwrite($handle, $data); + rewind($handle); + $zipFile = self::openFromStream($handle); + $zipFile->length = strlen($data); + return $zipFile; + } + + /** + * Returns the number of entries in this ZIP file. + * + * @return int + */ + public function count() + { + return sizeof($this->entries); + } + + /** + * Returns the list files. + * + * @return string[] + */ + public function getListFiles() + { + return array_keys($this->entries); + } + + /** + * @api + * @return ZipEntry[] + */ + public function getRawEntries() + { + return $this->entries; + } + + /** + * Checks whether a entry exists + * + * @param string $entryName + * @return bool + */ + public function hasEntry($entryName) + { + return isset($this->entries[$entryName]); + } + + /** + * Check whether the directory entry. + * Returns true if and only if this ZIP entry represents a directory entry + * (i.e. end with '/'). + * + * @param string $entryName + * @return bool + * @throws ZipNotFoundEntry + */ + public function isDirectory($entryName) + { + if (!isset($this->entries[$entryName])) { + throw new ZipNotFoundEntry('Zip entry ' . $entryName . ' not found'); + } + return $this->entries[$entryName]->isDirectory(); + } + + /** + * Set password to all encrypted entries. + * + * @param string $password Password + */ + public function setPassword($password) + { + foreach ($this->entries as $entry) { + if ($entry->isEncrypted()) { + $entry->setPassword($password); + } + } + } + + /** + * Set password to concrete zip entry. + * + * @param string $entryName Zip entry name + * @param string $password Password + * @throws ZipNotFoundEntry if don't exist zip entry. + */ + public function setEntryPassword($entryName, $password) + { + if (!isset($this->entries[$entryName])) { + throw new ZipNotFoundEntry('Zip entry ' . $entryName . ' not found'); + } + $entry = $this->entries[$entryName]; + if ($entry->isEncrypted()) { + $entry->setPassword($password); + } + } + + /** + * Returns the file comment. + * + * @return string The file comment. + */ + public function getComment() + { + return null === $this->comment ? '' : $this->decode($this->comment); + } + + /** + * Decode charset entry name. + * + * @param string $text + * @return string + */ + private function decode($text) + { + $inCharset = mb_detect_encoding($text, mb_detect_order(), true); + if ($inCharset === $this->charset) return $text; + return iconv($inCharset, $this->charset, $text); + } + + /** + * Returns entry comment. + * + * @param string $entryName + * @return string + * @throws ZipNotFoundEntry + */ + public function getEntryComment($entryName) + { + if (!isset($this->entries[$entryName])) { + throw new ZipNotFoundEntry("Not found entry " . $entryName); + } + return $this->entries[$entryName]->getComment(); + } + + /** + * Returns the name of the character set which is effectively used for + * decoding entry names and the file comment. + * + * @return string + */ + public function getCharset() + { + return $this->charset; + } + + /** + * Returns the file length of this ZIP file in bytes. + * + * @return int + */ + public function length() + { + return $this->length; + } + + /** + * Get info by entry. + * + * @param string|ZipEntry $entryName + * @return ZipInfo + * @throws ZipNotFoundEntry + */ + public function getEntryInfo($entryName) + { + if ($entryName instanceof ZipEntry) { + $entryName = $entryName->getName(); + } + if (!isset($this->entries[$entryName])) { + throw new ZipNotFoundEntry('Zip entry ' . $entryName . ' not found'); + } + $entry = $this->entries[$entryName]; + + return new ZipInfo($entry); + } + + /** + * Get info by all entries. + * + * @return ZipInfo[] + */ + public function getAllInfo() + { + return array_map([$this, 'getEntryInfo'], $this->entries); + } + + /** + * Extract the archive contents + * + * Extract the complete archive or the given files to the specified destination. + * + * @param string $destination Location where to extract the files. + * @param array $entries The entries to extract. It accepts + * either a single entry name or an array of names. + * @return bool + * @throws ZipException + */ + public function extractTo($destination, $entries = null) + { + if ($this->entries === null) { + throw new ZipException("Zip entries not initial"); + } + if (!file_exists($destination)) { + throw new ZipException("Destination " . $destination . " not found"); + } + if (!is_dir($destination)) { + throw new ZipException("Destination is not directory"); + } + if (!is_writable($destination)) { + throw new ZipException("Destination is not writable directory"); + } + + /** + * @var ZipEntry[] $zipEntries + */ + if (!empty($entries)) { + if (is_string($entries)) { + $entries = (array)$entries; + } + if (is_array($entries)) { + $flipEntries = array_flip($entries); + $zipEntries = array_filter($this->entries, function ($zipEntry) use ($flipEntries) { + /** + * @var ZipEntry $zipEntry + */ + return isset($flipEntries[$zipEntry->getName()]); + }); + } + } else { + $zipEntries = $this->entries; + } + + $extract = 0; + foreach ($zipEntries AS $entry) { + $file = $destination . DIRECTORY_SEPARATOR . $entry->getName(); + if ($entry->isDirectory()) { + if (!is_dir($file)) { + if (!mkdir($file, 0755, true)) { + throw new ZipException("Can not create dir " . $file); + } + chmod($file, 0755); + touch($file, $entry->getTime()); + } + continue; + } + $dir = dirname($file); + if (!file_exists($dir)) { + if (!mkdir($dir, 0755, true)) { + throw new ZipException("Can not create dir " . $dir); + } + chmod($dir, 0755); + touch($file, $entry->getTime()); + } + if (file_put_contents($file, $this->getEntryContent($entry->getName())) === null) { + return false; + } + touch($file, $entry->getTime()); + $extract++; + } + return $extract > 0; + } + + /** + * Returns an string content of the given entry. + * + * @param string $entryName + * @return string|null + * @throws ZipException + */ + public function getEntryContent($entryName) + { + if (!isset($this->entries[$entryName])) { + throw new ZipNotFoundEntry('Zip entry ' . $entryName . ' not found'); + } + $entry = $this->entries[$entryName]; + + $pos = $entry->getOffset(); + assert(ZipEntry::UNKNOWN !== $pos); + $startPos = $pos = $this->mapper->map($pos); + fseek($this->inputStream, $pos, SEEK_SET); + $localFileHeaderSig = current(unpack('V', fread($this->inputStream, 4))); + if (self::LOCAL_FILE_HEADER_SIG !== $localFileHeaderSig) { + throw new ZipException($entry->getName() . " (expected Local File Header)"); + } + fseek($this->inputStream, $pos + self::LOCAL_FILE_HEADER_FILE_NAME_LENGTH_POS, SEEK_SET); + $unpack = unpack('vfileLen/vextraLen', fread($this->inputStream, 4)); + $pos += self::LOCAL_FILE_HEADER_MIN_LEN + $unpack['fileLen'] + $unpack['extraLen']; + + assert(ZipEntry::UNKNOWN !== $entry->getCrc()); + + $check = $entry->isEncrypted(); + $method = $entry->getMethod(); + + $password = $entry->getPassword(); + if ($entry->isEncrypted() && empty($password)) { + throw new ZipException("Not set password"); + } + // Strong Encryption Specification - WinZip AES + if ($entry->isEncrypted() && ZipEntry::WINZIP_AES === $method) { + fseek($this->inputStream, $pos, SEEK_SET); + $winZipAesEngine = new WinZipAesEngine($entry); + $content = $winZipAesEngine->decrypt($this->inputStream); + // Disable redundant CRC-32 check. + $check = false; + + /** + * @var WinZipAesEntryExtraField $field + */ + $field = $entry->getExtraField(WinZipAesEntryExtraField::getHeaderId()); + $method = $field->getMethod(); + $entry->setEncryptionMethod(ZipEntry::ENCRYPTION_METHOD_WINZIP_AES); + } else { + // Get raw entry content + $content = stream_get_contents($this->inputStream, $entry->getCompressedSize(), $pos); + + // Traditional PKWARE Decryption + if ($entry->isEncrypted()) { + $zipCryptoEngine = new TraditionalPkwareEncryptionEngine($entry); + $content = $zipCryptoEngine->decrypt($content); + + $entry->setEncryptionMethod(ZipEntry::ENCRYPTION_METHOD_TRADITIONAL); + } + } + if ($check) { + // 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->inputStream, $pos + $entry->getCompressedSize(), SEEK_SET); + $localCrc = current(unpack('V', fread($this->inputStream, 4))); + if (self::DATA_DESCRIPTOR_SIG === $localCrc) { + $localCrc = current(unpack('V', fread($this->inputStream, 4))); + } + } else { + fseek($this->inputStream, $startPos + 14, SEEK_SET); + // The CRC32 in the Local File Header. + $localCrc = current(unpack('V', fread($this->inputStream, 4))); + } + if ($entry->getCrc() !== $localCrc) { + throw new Crc32Exception($entry->getName(), $entry->getCrc(), $localCrc); + } + } + + switch ($method) { + case ZipEntry::METHOD_STORED: + break; + case ZipEntry::METHOD_DEFLATED: + $content = gzinflate($content); + break; + case ZipEntry::METHOD_BZIP2: + if (!extension_loaded('bz2')) { + throw new ZipException('Extension bzip2 not install'); + } + $content = bzdecompress($content); + break; + default: + throw new ZipUnsupportMethod($entry->getName() + . " (compression method " + . $method + . " is not supported)"); + } + if ($check) { + $localCrc = crc32($content); + if ($entry->getCrc() !== $localCrc) { + if ($entry->isEncrypted()) { + throw new ZipCryptoException("Wrong password"); + } + throw new Crc32Exception($entry->getName(), $entry->getCrc(), $localCrc); + } + } + return $content; + } + + /** + * Release all resources + */ + function __destruct() + { + $this->close(); + } + + /** + * Close zip archive and release input stream. + */ + public function close() + { + $this->length = null; + + if ($this->inputStream !== null) { + fclose($this->inputStream); + $this->inputStream = null; + } + } + + /** + * Whether a offset exists + * @link http://php.net/manual/en/arrayaccess.offsetexists.php + * @param string $entryName An offset to check for. + * @return boolean true on success or false on failure. + * The return value will be casted to boolean if non-boolean was returned. + */ + public function offsetExists($entryName) + { + return isset($this->entries[$entryName]); + } + + /** + * Offset to retrieve + * @link http://php.net/manual/en/arrayaccess.offsetget.php + * @param string $entryName The offset to retrieve. + * @return string|null + */ + public function offsetGet($entryName) + { + return $this->offsetExists($entryName) ? $this->getEntryContent($entryName) : null; + } + + /** + * Offset to set + * @link http://php.net/manual/en/arrayaccess.offsetset.php + * @param string $entryName The offset to assign the value to. + * @param mixed $value The value to set. + * @throws ZipUnsupportMethod + */ + public function offsetSet($entryName, $value) + { + throw new ZipUnsupportMethod('Zip-file is read-only. This operation is prohibited.'); + } + + /** + * Offset to unset + * @link http://php.net/manual/en/arrayaccess.offsetunset.php + * @param string $entryName The offset to unset. + * @throws ZipUnsupportMethod + */ + public function offsetUnset($entryName) + { + throw new ZipUnsupportMethod('Zip-file is read-only. This operation is prohibited.'); + } + + /** + * Return the current element + * @link http://php.net/manual/en/iterator.current.php + * @return mixed Can return any type. + * @since 5.0.0 + */ + public function current() + { + return $this->offsetGet($this->key()); + } + + /** + * Move forward to next element + * @link http://php.net/manual/en/iterator.next.php + * @return void Any returned value is ignored. + * @since 5.0.0 + */ + public function next() + { + next($this->entries); + } + + /** + * Return the key of the current element + * @link http://php.net/manual/en/iterator.key.php + * @return mixed scalar on success, or null on failure. + * @since 5.0.0 + */ + public function key() + { + return key($this->entries); + } + + /** + * Checks if current position is valid + * @link http://php.net/manual/en/iterator.valid.php + * @return boolean The return value will be casted to boolean and then evaluated. + * Returns true on success or false on failure. + * @since 5.0.0 + */ + public function valid() + { + return $this->offsetExists($this->key()); + } + + /** + * Rewind the Iterator to the first element + * @link http://php.net/manual/en/iterator.rewind.php + * @return void Any returned value is ignored. + * @since 5.0.0 + */ + public function rewind() + { + reset($this->entries); + } +} \ No newline at end of file diff --git a/src/PhpZip/ZipOutputFile.php b/src/PhpZip/ZipOutputFile.php new file mode 100644 index 0000000..57bfdfe --- /dev/null +++ b/src/PhpZip/ZipOutputFile.php @@ -0,0 +1,1400 @@ + 'application/zip', + 'apk' => 'application/vnd.android.package-archive', + 'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + 'jar' => 'application/java-archive', + 'epub' => 'application/epub+zip' + ]; + + /** + * The charset to use for entry names and comments. + * + * @var string + */ + private $charset = 'UTF-8'; + + /** + * The file comment. + * + * @var null|string + */ + private $comment; + + /** + * Output zip entries. + * + * @var ZipOutputEntry[] + */ + private $entries = []; + + /** + * Start of central directory. + * + * @var int + */ + private $cdOffset; + + /** + * Default compression level for the methods DEFLATED and BZIP2. + * + * @var int + */ + private $level = self::LEVEL_DEFAULT_COMPRESSION; + + /** + * ZipOutputFile constructor. + * @param ZipFile|null $zipFile + */ + public function __construct(ZipFile $zipFile = null) + { + if ($zipFile !== null) { + $this->charset = $zipFile->getCharset(); + $this->comment = $zipFile->getComment(); + foreach ($zipFile->getRawEntries() as $entry) { + $this->entries[$entry->getName()] = new ZipOutputZipFileEntry($zipFile, $entry); + } + } + } + + /** + * Create empty archive + * + * @return ZipOutputFile + * @see ZipOutputFile::__construct() + */ + public static function create() + { + return new self(); + } + + /** + * Open zip archive from update. + * + * @param ZipFile $zipFile + * @return ZipOutputFile + * @throws IllegalArgumentException + * @see ZipOutputFile::__construct() + */ + public static function openFromZipFile(ZipFile $zipFile) + { + if ($zipFile === null) { + throw new IllegalArgumentException("Zip file is null"); + } + return new self($zipFile); + } + + /** + * Returns the list files. + * + * @return string[] + */ + public function getListFiles() + { + return array_keys($this->entries); + } + + /** + * Extract the archive contents + * + * Extract the complete archive or the given files to the specified destination. + * + * @param string $destination Location where to extract the files. + * @param array $entries The entries to extract. It accepts + * either a single entry name or an array of names. + * @return bool + * @throws ZipException + */ + public function extractTo($destination, $entries = null) + { + if ($this->entries === null) { + throw new ZipException("Zip entries not initial"); + } + if (!file_exists($destination)) { + throw new ZipException("Destination " . $destination . " not found"); + } + if (!is_dir($destination)) { + throw new ZipException("Destination is not directory"); + } + if (!is_writable($destination)) { + throw new ZipException("Destination is not writable directory"); + } + + /** + * @var ZipOutputEntry[] $zipOutputEntries + */ + if (!empty($entries)) { + if (is_string($entries)) { + $entries = (array)$entries; + } + if (is_array($entries)) { + $flipEntries = array_flip($entries); + $zipOutputEntries = array_filter($this->entries, function ($zipOutputEntry) use ($flipEntries) { + /** + * @var ZipOutputEntry $zipOutputEntry + */ + return isset($flipEntries[$zipOutputEntry->getEntry()->getName()]); + }); + } + } else { + $zipOutputEntries = $this->entries; + } + + $extract = 0; + foreach ($zipOutputEntries AS $outputEntry) { + $entry = $outputEntry->getEntry(); + $file = $destination . DIRECTORY_SEPARATOR . $entry->getName(); + if ($entry->isDirectory()) { + if (!is_dir($file)) { + if (!mkdir($file, 0755, true)) { + throw new ZipException("Can not create dir " . $file); + } + chmod($file, 0755); + touch($file, $entry->getTime()); + } + continue; + } + $dir = dirname($file); + if (!file_exists($dir)) { + if (!mkdir($dir, 0755, true)) { + throw new ZipException("Can not create dir " . $dir); + } + chmod($dir, 0755); + touch($file, $entry->getTime()); + } + if (file_put_contents($file, $this->getEntryContent($entry->getName())) === null) { + return false; + } + touch($file, $entry->getTime()); + $extract++; + } + return $extract > 0; + } + + /** + * Returns entry content. + * + * @param string $entryName + * @return string + * @throws ZipNotFoundEntry + */ + public function getEntryContent($entryName) + { + $entryName = (string)$entryName; + if (!isset($this->entries[$entryName])) { + throw new ZipNotFoundEntry('Can not entry ' . $entryName); + } + return $this->entries[$entryName]->getEntryContent(); + } + + /** + * @return null|string + */ + public function getComment() + { + return $this->comment; + } + + /** + * @param null|string $comment + * @throws IllegalArgumentException Length comment out of range + */ + public function setComment($comment) + { + if (null !== $comment && !empty($comment)) { + $comment = (string)$comment; + $length = strlen($comment); + if (0x0000 > $length || $length > 0xffff) { + throw new IllegalArgumentException('Length comment out of range'); + } + $this->comment = $comment; + } else { + $this->comment = null; + } + } + + /** + * Add entry from the string. + * + * @param string $entryName + * @param string $data String contents + * @param int $compressionMethod + * @throws IllegalArgumentException + */ + public function addFromString($entryName, $data, $compressionMethod = ZipEntry::METHOD_DEFLATED) + { + $entryName = (string)$entryName; + if ($data === null) { + throw new IllegalArgumentException("data is null"); + } + if (empty($entryName)) { + throw new IllegalArgumentException("Incorrect entry name " . $entryName); + } + $this->validateCompressionMethod($compressionMethod); + + $entry = new ZipEntry($entryName); + $entry->setMethod($compressionMethod); + $entry->setTime(time()); + + $this->entries[$entryName] = new ZipOutputStringEntry($data, $entry); + } + + /** + * Validate compression method. + * + * @param int $compressionMethod + * @throws IllegalArgumentException + * @see ZipEntry::METHOD_STORED + * @see ZipEntry::METHOD_DEFLATED + * @see ZipEntry::METHOD_BZIP2 + */ + private function validateCompressionMethod($compressionMethod) + { + if (!in_array($compressionMethod, self::$allowCompressionMethods, true)) { + throw new IllegalArgumentException("Compression method " . $compressionMethod . ' is not support'); + } + } + + /** + * Add directory to the zip archive. + * + * @param string $inputDir Input directory + * @param bool $recursive Recursive search files + * @param string|null $moveToPath If not null then put $inputDir to path $outEntryDir + * @param array $ignoreFiles List of files to exclude from the folder $inputDir. + * @param int $compressionMethod Compression method + * @return bool + * @throws IllegalArgumentException + */ + public function addDir( + $inputDir, + $recursive = true, + $moveToPath = "/", + array $ignoreFiles = [], + $compressionMethod = ZipEntry::METHOD_DEFLATED + ) + { + $inputDir = (string)$inputDir; + if (empty($inputDir)) { + throw new IllegalArgumentException('Input dir empty'); + } + if (!is_dir($inputDir)) { + throw new IllegalArgumentException('Directory ' . $inputDir . ' can\'t exists'); + } + $this->validateCompressionMethod($compressionMethod); + + if (null !== $moveToPath && is_string($moveToPath) && !empty($moveToPath)) { + $moveToPath = rtrim($moveToPath, '/') . '/'; + } else { + $moveToPath = "/"; + } + $inputDir = rtrim($inputDir, '/\\') . DIRECTORY_SEPARATOR; + + $count = $this->count(); + + $files = FilesUtil::fileSearchWithIgnore($inputDir, $recursive, $ignoreFiles); + /** + * @var \SplFileInfo $file + */ + foreach ($files as $file) { + $filename = str_replace($inputDir, $moveToPath, $file); + $filename = ltrim($filename, '/'); + is_dir($file) && FilesUtil::isEmptyDir($file) && $this->addEmptyDir($filename); + is_file($file) && $this->addFromFile($file, $filename, $compressionMethod); + } + return $this->count() > $count; + } + + /** + * Count zip entries. + * + * @return int + */ + public function count() + { + return sizeof($this->entries); + } + + /** + * Add an empty directory in the zip archive. + * + * @param string $dirName + * @throws IllegalArgumentException + */ + public function addEmptyDir($dirName) + { + $dirName = (string)$dirName; + if (empty($dirName)) { + throw new IllegalArgumentException("dirName null or not string"); + } + $dirName = rtrim($dirName, '/') . '/'; + if (!isset($this->entries[$dirName])) { + $entry = new ZipEntry($dirName); + $entry->setTime(time()); + $entry->setMethod(ZipEntry::METHOD_STORED); + $entry->setSize(0); + $entry->setCompressedSize(0); + $entry->setCrc(0); + + $this->entries[$dirName] = new ZipOutputEmptyDirEntry($entry); + } + } + + /** + * Add entry from the file. + * + * @param string $filename + * @param string|null $entryName + * @param int $compressionMethod + * @throws IllegalArgumentException + */ + public function addFromFile($filename, $entryName = null, $compressionMethod = ZipEntry::METHOD_DEFLATED) + { + if ($filename === null) { + throw new IllegalArgumentException("Filename is null"); + } + if (!is_file($filename)) { + throw new IllegalArgumentException("File is not exists"); + } + if (!($handle = fopen($filename, 'rb'))) { + throw new IllegalArgumentException('File ' . $filename . ' can not open.'); + } + if ($entryName === null) { + $entryName = basename($filename); + } + $this->addFromStream($handle, $entryName, $compressionMethod); + } + + /** + * Add entry from the stream. + * + * @param resource $stream Stream resource + * @param string $entryName + * @param int $compressionMethod + * @throws IllegalArgumentException + */ + public function addFromStream($stream, $entryName, $compressionMethod = ZipEntry::METHOD_DEFLATED) + { + if (!is_resource($stream)) { + throw new IllegalArgumentException("stream is not resource"); + } + $entryName = (string)$entryName; + if (empty($entryName)) { + throw new IllegalArgumentException("Incorrect entry name " . $entryName); + } + $this->validateCompressionMethod($compressionMethod); + + $entry = new ZipEntry($entryName); + $entry->setMethod($compressionMethod); + $entry->setTime(time()); + + $this->entries[$entryName] = new ZipOutputStreamEntry($stream, $entry); + } + + /** + * Add files from glob pattern. + * + * @param string $inputDir Input directory + * @param string $globPattern Glob pattern. + * @param bool $recursive Recursive search. + * @param string|null $moveToPath Add files to this directory, or the root. + * @param int $compressionMethod Compression method. + * @return bool + * @throws IllegalArgumentException + * @sse https://en.wikipedia.org/wiki/Glob_(programming) Glob pattern syntax + */ + public function addFilesFromGlob( + $inputDir, + $globPattern, + $recursive = true, + $moveToPath = '/', + $compressionMethod = ZipEntry::METHOD_DEFLATED + ) + { + $inputDir = (string)$inputDir; + if (empty($inputDir)) { + throw new IllegalArgumentException('Input dir empty'); + } + if (!is_dir($inputDir)) { + throw new IllegalArgumentException('Directory ' . $inputDir . ' can\'t exists'); + } + if (null === $globPattern || !is_string($globPattern)) { + throw new IllegalArgumentException("globPattern null"); + } + if (empty($globPattern)) { + throw new IllegalArgumentException("globPattern empty"); + } + $this->validateCompressionMethod($compressionMethod); + + $inputDir = rtrim($inputDir, '/\\') . DIRECTORY_SEPARATOR; + $globPattern = $inputDir . $globPattern; + + $filesFound = FilesUtil::globFileSearch($globPattern, GLOB_BRACE, $recursive); + if ($filesFound === false || empty($filesFound)) { + return false; + } + if (!empty($moveToPath) && is_string($moveToPath)) { + $moveToPath = rtrim($moveToPath, '/') . '/'; + } else { + $moveToPath = "/"; + } + + $count = $this->count(); + /** + * @var string $file + */ + foreach ($filesFound as $file) { + $filename = str_replace($inputDir, $moveToPath, $file); + $filename = ltrim($filename, '/'); + is_dir($file) && FilesUtil::isEmptyDir($file) && $this->addEmptyDir($filename); + is_file($file) && $this->addFromFile($file, $filename, $compressionMethod); + } + return $this->count() > $count; + } + + /** + * Add files from regex pattern. + * + * @param string $inputDir Search files in this directory. + * @param string $regexPattern Regex pattern. + * @param bool $recursive Recursive search. + * @param string|null $moveToPath Add files to this directory, or the root. + * @param int $compressionMethod Compression method. + * @return bool + * @throws IllegalArgumentException + */ + public function addFilesFromRegex( + $inputDir, + $regexPattern, + $recursive = true, + $moveToPath = "/", + $compressionMethod = ZipEntry::METHOD_DEFLATED + ) + { + if ($regexPattern === null || !is_string($regexPattern) || empty($regexPattern)) { + throw new IllegalArgumentException("regex pattern empty"); + } + $inputDir = (string)$inputDir; + if (empty($inputDir)) { + throw new IllegalArgumentException('Invalid $inputDir value'); + } + if (!is_dir($inputDir)) { + throw new IllegalArgumentException('Path ' . $inputDir . ' can\'t directory.'); + } + $this->validateCompressionMethod($compressionMethod); + + $inputDir = rtrim($inputDir, '/\\') . DIRECTORY_SEPARATOR; + + $files = FilesUtil::regexFileSearch($inputDir, $regexPattern, $recursive); + if ($files === false || empty($files)) { + return false; + } + if (!empty($moveToPath) && is_string($moveToPath)) { + $moveToPath = rtrim($moveToPath, '/') . '/'; + } else { + $moveToPath = "/"; + } + $inputDir = rtrim($inputDir, '/\\') . DIRECTORY_SEPARATOR; + + $count = $this->count(); + /** + * @var string $file + */ + foreach ($files as $file) { + $filename = str_replace($inputDir, $moveToPath, $file); + $filename = ltrim($filename, '/'); + is_dir($file) && FilesUtil::isEmptyDir($file) && $this->addEmptyDir($filename); + is_file($file) && $this->addFromFile($file, $filename, $compressionMethod); + } + return $this->count() > $count; + } + + /** + * Rename the entry. + * + * @param string $oldName Old entry name. + * @param string $newName New entry name. + * @throws IllegalArgumentException + * @throws ZipNotFoundEntry + */ + public function rename($oldName, $newName) + { + if ($oldName === null || $newName === null) { + throw new IllegalArgumentException("name is null"); + } + $oldName = (string)$oldName; + $newName = (string)$newName; + if (!isset($this->entries[$oldName])) { + throw new ZipNotFoundEntry("Not found entry " . $oldName); + } + if (isset($this->entries[$newName])) { + throw new IllegalArgumentException("New entry name " . $newName . ' is exists.'); + } + $this->entries[$newName] = $this->entries[$oldName]; + unset($this->entries[$oldName]); + $this->entries[$newName]->getEntry()->setName($newName); + } + + /** + * Delete entry by name. + * + * @param string $entryName + * @throws ZipNotFoundEntry + */ + public function deleteFromName($entryName) + { + $entryName = (string)$entryName; + if (!isset($this->entries[$entryName])) { + throw new ZipNotFoundEntry("Not found entry " . $entryName); + } + unset($this->entries[$entryName]); + } + + /** + * Delete entries by glob pattern. + * + * @param string $globPattern Glob pattern + * @return bool + * @throws IllegalArgumentException + * @sse https://en.wikipedia.org/wiki/Glob_(programming) Glob pattern syntax + */ + public function deleteFromGlob($globPattern) + { + if ($globPattern === null || !is_string($globPattern) || empty($globPattern)) { + throw new IllegalArgumentException("Glob pattern is empty"); + } + $globPattern = '~' . FilesUtil::convertGlobToRegEx($globPattern) . '~si'; + return $this->deleteFromRegex($globPattern); + } + + /** + * Delete entries by regex pattern. + * + * @param string $regexPattern Regex pattern + * @return bool + * @throws IllegalArgumentException + */ + public function deleteFromRegex($regexPattern) + { + if ($regexPattern === null || !is_string($regexPattern) || empty($regexPattern)) { + throw new IllegalArgumentException("Regex pattern is empty."); + } + $count = $this->count(); + foreach ($this->entries as $entryName => $entry) { + if (preg_match($regexPattern, $entryName)) { + unset($this->entries[$entryName]); + } + } + return $this->count() > $count; + } + + /** + * Delete all entries + */ + public function deleteAll() + { + unset($this->entries); // for stream close + $this->entries = []; + } + + /** + * Set the compression method for a concrete entry. + * + * @param string $entryName + * @param int $compressionMethod + * @throws ZipNotFoundEntry + * @see ZipEntry::METHOD_STORED + * @see ZipEntry::METHOD_DEFLATED + * @see ZipEntry::METHOD_BZIP2 + */ + public function setCompressionMethod($entryName, $compressionMethod = ZipEntry::METHOD_DEFLATED) + { + $entryName = (string)$entryName; + if (!isset($this->entries[$entryName])) { + throw new ZipNotFoundEntry("Not found entry " . $entryName); + } + $this->validateCompressionMethod($compressionMethod); + $this->entries[$entryName]->getEntry()->setMethod($compressionMethod); + } + + /** + * Returns the comment from the entry. + * + * @param string $entryName + * @return string|null + * @throws ZipNotFoundEntry + */ + public function getEntryComment($entryName) + { + $entryName = (string)$entryName; + if (!isset($this->entries[$entryName])) { + throw new ZipNotFoundEntry("Not found entry " . $entryName); + } + return $this->entries[$entryName]->getEntry()->getComment(); + } + + /** + * Set entry comment. + * + * @param string $entryName + * @param string|null $comment + * @throws ZipNotFoundEntry + */ + public function setEntryComment($entryName, $comment = null) + { + $entryName = (string)$entryName; + if (!isset($this->entries[$entryName])) { + throw new ZipNotFoundEntry("Not found entry " . $entryName); + } + $this->entries[$entryName]->getEntry()->setComment($comment); + } + + /** + * Set password for all previously added entries. + * For the following entries, set the password separately, + * or set a password before saving archive so that it applies to all entries. + * + * @param string $password If password null then encryption clear + * @param int $encryptionMethod Encryption method + */ + public function setPassword($password, $encryptionMethod = ZipEntry::ENCRYPTION_METHOD_WINZIP_AES) + { + foreach ($this->entries as $outputEntry) { + $outputEntry->getEntry()->setPassword($password, $encryptionMethod); + } + } + + /** + * Set a password and encryption method for a concrete entry. + * + * @param string $entryName Zip entry name + * @param string $password If password null then encryption clear + * @param int $encryptionMethod Encryption method + * @throws ZipNotFoundEntry + * @see ZipEntry::ENCRYPTION_METHOD_TRADITIONAL + * @see ZipEntry::ENCRYPTION_METHOD_WINZIP_AES + */ + public function setEntryPassword($entryName, $password, $encryptionMethod = ZipEntry::ENCRYPTION_METHOD_WINZIP_AES) + { + $entryName = (string)$entryName; + if (!isset($this->entries[$entryName])) { + throw new ZipNotFoundEntry("Not found entry " . $entryName); + } + $entry = $this->entries[$entryName]->getEntry(); + $entry->setPassword($password, $encryptionMethod); + } + + /** + * Remove password from all entries + */ + public function removePasswordAllEntries() + { + foreach ($this->entries as $outputEntry) { + $zipEntry = $outputEntry->getEntry(); + $zipEntry->clearEncryption(); + } + } + + /** + * Remove password for concrete zip entry. + * + * @param string $entryName + * @throws ZipNotFoundEntry + */ + public function removePasswordFromEntry($entryName) + { + $entryName = (string)$entryName; + if (!isset($this->entries[$entryName])) { + throw new ZipNotFoundEntry("Not found entry " . $entryName); + } + $zipEntry = $this->entries[$entryName]->getEntry(); + $zipEntry->clearEncryption(); + } + + /** + * Returns the compression level for entries. + * This property is only used if the effective compression method is DEFLATED or BZIP2 + * + * @return int The compression level for entries. + * @see ZipOutputFile::setLevel() + */ + public function getLevel() + { + return $this->level; + } + + /** + * Sets the compression level for entries. + * This property is only used if the effective compression method is DEFLATED or BZIP2. + * Legal values are ZipOutputFile::LEVEL_DEFAULT_COMPRESSION or range from + * ZipOutputFile::LEVEL_BEST_SPEED to ZipOutputFile::LEVEL_BEST_COMPRESSION. + * + * @param int $level the compression level for entries. + * @throws IllegalArgumentException if the compression level is invalid. + * @see ZipOutputFile::getLevel() + */ + public function setLevel($level) + { + if ( + ($level < self::LEVEL_BEST_SPEED || self::LEVEL_BEST_COMPRESSION < $level) + && self::LEVEL_DEFAULT_COMPRESSION !== $level + ) { + throw new IllegalArgumentException("Invalid compression level!"); + } + $this->level = $level; + } + + /** + * Save as file + * + * @param string $filename Output filename + * @throws IllegalArgumentException + * @throws ZipException + */ + public function saveAsFile($filename) + { + $filename = (string)$filename; + + $tempFilename = $filename . '.temp' . uniqid(); + if (!($handle = fopen($tempFilename, 'w+b'))) { + throw new IllegalArgumentException("File " . $tempFilename . ' can not open from write.'); + } + $this->saveAsStream($handle); + + if (!rename($tempFilename, $filename)) { + throw new ZipException('Can not move ' . $tempFilename . ' to ' . $filename); + } + } + + /** + * Save as stream + * + * @param resource $handle Output stream resource + * @param bool $autoClose Close the stream resource, if found true. + * @throws IllegalArgumentException + */ + public function saveAsStream($handle, $autoClose = true) + { + if (!is_resource($handle)) { + throw new IllegalArgumentException('handle is not resource'); + } + ftruncate($handle, 0); + foreach ($this->entries as $key => $outputEntry) { + $this->writeEntry($handle, $outputEntry); + } + $this->cdOffset = ftell($handle); + foreach ($this->entries as $key => $outputEntry) { + if (!$this->writeCentralFileHeader($handle, $outputEntry->getEntry())) { + unset($this->entries[$key]); + } + } + $this->writeEndOfCentralDirectory($handle); + if ($autoClose) { + fclose($handle); + } + } + + /** + * Write entry. + * + * @param resource $outputHandle Output stream resource. + * @param ZipOutputEntry $outputEntry + * @throws ZipException + */ + private function writeEntry($outputHandle, ZipOutputEntry $outputEntry) + { + $entry = $outputEntry->getEntry(); + $size = strlen($entry->getName()) + strlen($entry->getExtra()) + strlen($entry->getComment()); + if (0xffff < $size) { + throw new ZipException($entry->getName() + . " (the total size of " + . $size + . " bytes for the name, extra fields and comment exceeds the maximum size of " + . 0xffff . " bytes)"); + } + + if (ZipEntry::UNKNOWN === $entry->getPlatform()) { + $entry->setRawPlatform(ZipEntry::getCurrentPlatform()); + } + if (ZipEntry::UNKNOWN === $entry->getTime()) { + $entry->setTime(time()); + } + $method = $entry->getMethod(); + if (ZipEntry::UNKNOWN === $method) { + $entry->setRawMethod($method = ZipEntry::METHOD_DEFLATED); + } + $skipCrc = false; + + $encrypted = $entry->isEncrypted(); + $dd = $entry->isDataDescriptorRequired(); + // Compose General Purpose Bit Flag. + // See appendix D of PKWARE's ZIP File Format Specification. + $utf8 = true; + $general = ($encrypted ? ZipEntry::GPBF_ENCRYPTED : 0) + | ($dd ? ZipEntry::GPBF_DATA_DESCRIPTOR : 0) + | ($utf8 ? ZipEntry::GPBF_UTF8 : 0); + + $entryContent = $outputEntry->getEntryContent(); + + $entry->setRawSize(strlen($entryContent)); + $entry->setCrc(crc32($entryContent)); + + if ($encrypted && null === $entry->getPassword()) { + throw new ZipException("Can not password from entry " . $entry->getName()); + } + + if ( + $encrypted && + ( + ZipEntry::WINZIP_AES === $method || + $entry->getEncryptionMethod() === ZipEntry::ENCRYPTION_METHOD_WINZIP_AES + ) + ) { + $field = null; + $method = $entry->getMethod(); + $keyStrength = 256; // bits + + $compressedSize = $entry->getCompressedSize(); + + if (ZipEntry::WINZIP_AES === $method) { + /** + * @var WinZipAesEntryExtraField $field + */ + $field = $entry->getExtraField(WinZipAesEntryExtraField::getHeaderId()); + if (null !== $field) { + $method = $field->getMethod(); + if (ZipEntry::UNKNOWN !== $compressedSize) { + $compressedSize -= $field->getKeyStrength() / 2 // salt value + + 2 // password verification value + + 10; // authentication code + } + $entry->setRawMethod($method); + } + } + if (null === $field) { + $field = new WinZipAesEntryExtraField(); + } + $field->setKeyStrength($keyStrength); + $field->setMethod($method); + $size = $entry->getSize(); + if (20 <= $size && ZipEntry::METHOD_BZIP2 !== $method) { + $field->setVendorVersion(WinZipAesEntryExtraField::VV_AE_1); + } else { + $field->setVendorVersion(WinZipAesEntryExtraField::VV_AE_2); + $skipCrc = true; + } + $entry->addExtraField($field); + if (ZipEntry::UNKNOWN !== $compressedSize) { + $compressedSize += $field->getKeyStrength() / 2 // salt value + + 2 // password verification value + + 10; // authentication code + $entry->setRawCompressedSize($compressedSize); + } + if ($skipCrc) { + $entry->setRawCrc(0); + } + } + + switch ($method) { + case ZipEntry::METHOD_STORED: + break; + case ZipEntry::METHOD_DEFLATED: + $entryContent = gzdeflate($entryContent, $this->level); + break; + case ZipEntry::METHOD_BZIP2: + $entryContent = bzcompress( + $entryContent, + $this->level === self::LEVEL_DEFAULT_COMPRESSION ? 4 : $this->level + ); + break; + default: + throw new ZipException($entry->getName() . " (unsupported compression method " . $method . ")"); + } + + if ($encrypted) { + if ($entry->getEncryptionMethod() === ZipEntry::ENCRYPTION_METHOD_WINZIP_AES) { + if ($skipCrc) { + $entry->setRawCrc(0); + } + $entry->setRawMethod(ZipEntry::WINZIP_AES); + + /** + * @var WinZipAesEntryExtraField $field + */ + $field = $entry->getExtraField(WinZipAesEntryExtraField::getHeaderId()); + $winZipAesEngine = new WinZipAesEngine($entry, $field); + $entryContent = $winZipAesEngine->encrypt($entryContent); + } elseif ($entry->getEncryptionMethod() === ZipEntry::ENCRYPTION_METHOD_TRADITIONAL) { + $zipCryptoEngine = new TraditionalPkwareEncryptionEngine($entry); + $entryContent = $zipCryptoEngine->encrypt( + $entryContent, + ($dd ? ($entry->getRawTime() & 0x0000ffff) << 16 : $entry->getCrc()) + ); + } + } + + $compressedSize = strlen($entryContent); + $entry->setCompressedSize($compressedSize); + + $offset = ftell($outputHandle); + + // Start changes. + // 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 + $extra = $entry->getRawExtraFields(); + fwrite($outputHandle, pack('VvvvVVVVvv', + ZipConstants::LOCAL_FILE_HEADER_SIG, + $entry->getVersionNeededToExtract(), + $general, + $entry->getRawMethod(), + (int)$entry->getRawTime(), + $dd ? 0 : (int)$entry->getRawCrc(), + $dd ? 0 : (int)$entry->getRawCompressedSize(), + $dd ? 0 : (int)$entry->getRawSize(), + strlen($entry->getName()), + strlen($extra) + )); + // file name (variable size) + fwrite($outputHandle, $entry->getName()); + // extra field (variable size) + fwrite($outputHandle, $extra); + // Commit changes. + $entry->setGeneralPurposeBitFlags($general); + $entry->setRawOffset($offset); + + fwrite($outputHandle, $entryContent); + + assert(ZipEntry::UNKNOWN !== $entry->getCrc()); + assert(ZipEntry::UNKNOWN !== $entry->getSize()); + if ($entry->getGeneralPurposeBitFlag(ZipEntry::GPBF_DATA_DESCRIPTOR)) { + // data descriptor signature 4 bytes (0x08074b50) + // crc-32 4 bytes + fwrite($outputHandle, pack('VV', + ZipConstants::DATA_DESCRIPTOR_SIG, + (int)$entry->getRawCrc() + )); + // compressed size 4 or 8 bytes + // uncompressed size 4 or 8 bytes + if ($entry->isZip64ExtensionsRequired()) { + fwrite($outputHandle, PackUtil::packLongLE($compressedSize)); + fwrite($outputHandle, PackUtil::packLongLE($entry->getSize())); + } else { + fwrite($outputHandle, pack('VV', + (int)$entry->getRawCompressedSize(), + (int)$entry->getRawSize() + )); + } + } elseif ($entry->getCompressedSize() !== $compressedSize) { + throw new ZipException($entry->getName() + . " (expected compressed entry size of " + . $entry->getCompressedSize() . " bytes, but is actually " . $compressedSize . " bytes)"); + } + + } + + /** + * Writes a Central File Header record. + * + * @param resource $handle Output stream. + * @param ZipEntry $entry + * @return bool false if and only if the record has been skipped, + * i.e. not written for some other reason than an I/O error. + */ + private function writeCentralFileHeader($handle, ZipEntry $entry) + { + $compressedSize = $entry->getCompressedSize(); + $size = $entry->getSize(); + // This test MUST NOT include the CRC-32 because VV_AE_2 sets it to + // UNKNOWN! + if (ZipEntry::UNKNOWN === ($compressedSize | $size)) { + return false; + } + + // 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 datetime 4 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 + $extra = $entry->getRawExtraFields(); + $extraSize = strlen($extra); + fwrite($handle, pack('VvvvvVVVVvvvvvVV', + self::CENTRAL_FILE_HEADER_SIG, + ($entry->getRawPlatform() << 8) | 63, + $entry->getVersionNeededToExtract(), + $entry->getGeneralPurposeBitFlags(), + $entry->getRawMethod(), + (int)$entry->getRawTime(), + (int)$entry->getRawCrc(), + (int)$entry->getRawCompressedSize(), + (int)$entry->getRawSize(), + strlen($entry->getName()), + $extraSize, + strlen($entry->getComment()), + 0, + 0, + (int)$entry->getRawExternalAttributes(), + (int)$entry->getRawOffset() + )); + // file name (variable size) + fwrite($handle, $entry->getName()); + // extra field (variable size) + fwrite($handle, $extra); + // file comment (variable size) + fwrite($handle, $entry->getComment()); + return true; + } + + /** + * Write end of central directory. + * + * @param resource $handle Output stream resource + */ + private function writeEndOfCentralDirectory($handle) + { + $cdEntries = sizeof($this->entries); + $cdOffset = $this->cdOffset; + $cdSize = ftell($handle) - $cdOffset; + $cdEntriesZip64 = $cdEntries > 0xffff; + $cdSizeZip64 = $cdSize > 0xffffffff; + $cdOffsetZip64 = $cdOffset > 0xffffffff; + $cdEntries16 = $cdEntriesZip64 ? 0xffff : (int)$cdEntries; + $cdSize32 = $cdSizeZip64 ? 0xffffffff : $cdSize; + $cdOffset32 = $cdOffsetZip64 ? 0xffffffff : $cdOffset; + $zip64 // ZIP64 extensions? + = $cdEntriesZip64 + || $cdSizeZip64 + || $cdOffsetZip64; + if ($zip64) { + $zip64EndOfCentralDirectoryOffset // relative offset of the zip64 end of central directory record + = ftell($handle); + // zip64 end of central dir + // signature 4 bytes (0x06064b50) + fwrite($handle, pack('V', ZipConstants::ZIP64_END_OF_CENTRAL_DIRECTORY_RECORD_SIG)); + // size of zip64 end of central + // directory record 8 bytes + fwrite($handle, PackUtil::packLongLE(ZipConstants::ZIP64_END_OF_CENTRAL_DIRECTORY_RECORD_MIN_LEN - 12)); + // version made by 2 bytes + // version needed to extract 2 bytes + // due to potential use of BZIP2 compression + // number of this disk 4 bytes + // number of the disk with the + // start of the central directory 4 bytes + fwrite($handle, pack('vvVV', 63, 46, 0, 0)); + // total number of entries in the + // central directory on this disk 8 bytes + fwrite($handle, PackUtil::packLongLE($cdEntries)); + // total number of entries in the + // central directory 8 bytes + fwrite($handle, PackUtil::packLongLE($cdEntries)); + // size of the central directory 8 bytes + fwrite($handle, PackUtil::packLongLE($cdSize)); + // offset of start of central + // directory with respect to + // the starting disk number 8 bytes + fwrite($handle, PackUtil::packLongLE($cdOffset)); + // zip64 extensible data sector (variable size) + // + // zip64 end of central dir locator + // signature 4 bytes (0x07064b50) + // number of the disk with the + // start of the zip64 end of + // central directory 4 bytes + fwrite($handle, pack('VV', self::ZIP64_END_OF_CENTRAL_DIRECTORY_LOCATOR_SIG, 0)); + // relative offset of the zip64 + // end of central directory record 8 bytes + fwrite($handle, PackUtil::packLongLE($zip64EndOfCentralDirectoryOffset)); + // total number of disks 4 bytes + fwrite($handle, pack('V', 1)); + } + // 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 + $comment = $this->comment === null ? "" : $this->comment; + $commentLength = strlen($comment); + fwrite($handle, pack('VvvvvVVv', + self::END_OF_CENTRAL_DIRECTORY_RECORD_SIG, + 0, + 0, + $cdEntries16, + $cdEntries16, + (int)$cdSize32, + (int)$cdOffset32, + $commentLength + )); + if ($commentLength > 0) { + // .ZIP file comment (variable size) + fwrite($handle, $comment); + } + } + + /** + * Output .ZIP archive as attachment. + * Die after output. + * + * @param string $outputFilename + * @param string|null $mimeType + * @throws IllegalArgumentException + */ + public function outputAsAttachment($outputFilename, $mimeType = null) + { + $outputFilename = (string)$outputFilename; + if (empty($outputFilename)) { + throw new IllegalArgumentException("Output filename is empty."); + } + if (empty($mimeType) || !is_string($mimeType)) { + $ext = strtolower(pathinfo($outputFilename, PATHINFO_EXTENSION)); + + if (!empty($ext) && isset(self::$defaultMimeTypes[$ext])) { + $mimeType = self::$defaultMimeTypes[$ext]; + } else { + $mimeType = self::$defaultMimeTypes['zip']; + } + } + $outputFilename = basename($outputFilename); + + $content = $this->outputAsString(); + + header("Content-Type: " . $mimeType); + header("Content-Disposition: attachment; filename=" . rawurlencode($outputFilename)); + header("Content-Length: " . strlen($content)); + header("Accept-Ranges: bytes"); + + echo $content; + exit; + } + + /** + * Returns the zip archive as a string. + * + * @return string + * @throws IllegalArgumentException + */ + public function outputAsString() + { + if (!($handle = fopen('php://temp', 'w+b'))) { + throw new IllegalArgumentException("Temp file can not open from write."); + } + $this->saveAsStream($handle, false); + rewind($handle); + $content = stream_get_contents($handle); + fclose($handle); + return $content; + } + + /** + * Close zip archive. + * Release all resources. + */ + public function close() + { + unset($this->entries); + } + + /** + * Release all resources + */ + function __destruct() + { + $this->close(); + } + + /** + * Whether a offset exists + * @link http://php.net/manual/en/arrayaccess.offsetexists.php + * @param string $entryName An offset to check for. + * @return boolean true on success or false on failure. + * The return value will be casted to boolean if non-boolean was returned. + */ + public function offsetExists($entryName) + { + return isset($this->entries[$entryName]); + } + + /** + * Offset to retrieve + * @link http://php.net/manual/en/arrayaccess.offsetget.php + * @param string $entryName The offset to retrieve. + * @return string|null + */ + public function offsetGet($entryName) + { + return $this->offsetExists($entryName) ? $this->getEntryContent($entryName) : null; + } + + /** + * Offset to set. Create or modify zip entry. + * @link http://php.net/manual/en/arrayaccess.offsetset.php + * @param string $entryName The offset to assign the value to. + * @param string $uncompressedDataContent The value to set. + * @throws IllegalArgumentException + */ + public function offsetSet($entryName, $uncompressedDataContent) + { + if(empty($entryName)){ + throw new IllegalArgumentException('Entry name empty'); + } + if($entryName[strlen($entryName)-1] === '/'){ + $this->addEmptyDir($entryName); + } + else{ + $this->addFromString($entryName, $uncompressedDataContent); + } + } + + /** + * Offset to unset + * @link http://php.net/manual/en/arrayaccess.offsetunset.php + * @param string $entryName The offset to unset. + */ + public function offsetUnset($entryName) + { + $this->deleteFromName($entryName); + } + + /** + * Return the current element + * @link http://php.net/manual/en/iterator.current.php + * @return mixed Can return any type. + * @since 5.0.0 + */ + public function current() + { + return $this->offsetGet($this->key()); + } + + /** + * Move forward to next element + * @link http://php.net/manual/en/iterator.next.php + * @return void Any returned value is ignored. + * @since 5.0.0 + */ + public function next() + { + next($this->entries); + } + + /** + * Return the key of the current element + * @link http://php.net/manual/en/iterator.key.php + * @return mixed scalar on success, or null on failure. + * @since 5.0.0 + */ + public function key() + { + return key($this->entries); + } + + /** + * Checks if current position is valid + * @link http://php.net/manual/en/iterator.valid.php + * @return boolean The return value will be casted to boolean and then evaluated. + * Returns true on success or false on failure. + * @since 5.0.0 + */ + public function valid() + { + return $this->offsetExists($this->key()); + } + + /** + * Rewind the Iterator to the first element + * @link http://php.net/manual/en/iterator.rewind.php + * @return void Any returned value is ignored. + * @since 5.0.0 + */ + public function rewind() + { + reset($this->entries); + } +} \ No newline at end of file diff --git a/src/ZipEntry.php b/src/ZipEntry.php deleted file mode 100644 index 04e960f..0000000 --- a/src/ZipEntry.php +++ /dev/null @@ -1,905 +0,0 @@ - 'MS-DOS and OS/2 (FAT / VFAT / FAT32 file systems)', - 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 => 'OS X (Darwin)', - ); - - // constants version by extract - const EXTRACT_VERSION_10 = 10; - const EXTRACT_VERSION_11 = 11; - const EXTRACT_VERSION_20 = 20; -//1.0 - Default value -//1.1 - File is a volume label -//2.0 - File is a folder (directory) -//2.0 - File is compressed using Deflate compression -//2.0 - File is encrypted using traditional PKWARE encryption -//2.1 - File is compressed using Deflate64(tm) -//2.5 - File is compressed using PKWARE DCL Implode -//2.7 - File is a patch data set -//4.5 - File uses ZIP64 format extensions -//4.6 - File is compressed using BZIP2 compression* -//5.0 - File is encrypted using DES -//5.0 - File is encrypted using 3DES -//5.0 - File is encrypted using original RC2 encryption -//5.0 - File is encrypted using RC4 encryption -//5.1 - File is encrypted using AES encryption -//5.1 - File is encrypted using corrected RC2 encryption** -//5.2 - File is encrypted using corrected RC2-64 encryption** -//6.1 - File is encrypted using non-OAEP key wrapping*** -//6.2 - Central directory encryption -//6.3 - File is compressed using LZMA -//6.3 - File is compressed using PPMd+ -//6.3 - File is encrypted using Blowfish -//6.3 - File is encrypted using Twofish - - const FLAG_ENCRYPTION = 0; - const FLAG_DATA_DESCRIPTION = 3; - const FLAG_UTF8 = 11; - private static $valuesFlag = array( - self::FLAG_ENCRYPTION => 'encrypted file', // 1 << 0 - 1 => 'compression option', // 1 << 1 - 2 => 'compression option', // 1 << 2 - self::FLAG_DATA_DESCRIPTION => 'data descriptor', // 1 << 3 - 4 => 'enhanced deflation', // 1 << 4 - 5 => 'compressed patched data', // 1 << 5 - 6 => 'strong encryption', // 1 << 6 - 7 => 'unused', // 1 << 7 - 8 => 'unused', // 1 << 8 - 9 => 'unused', // 1 << 9 - 10 => 'unused', // 1 << 10 - self::FLAG_UTF8 => 'language encoding', // 1 << 11 - 12 => 'reserved', // 1 << 12 - 13 => 'mask header values', // 1 << 13 - 14 => 'reserved', // 1 << 14 - 15 => 'reserved', // 1 << 15 - ); - - // compression method constants - const COMPRESS_METHOD_STORED = 0; - const COMPRESS_METHOD_DEFLATED = 8; - const COMPRESS_METHOD_AES = 99; - - private static $valuesCompressionMethod = array( - self::COMPRESS_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', - self::COMPRESS_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', - self::COMPRESS_METHOD_AES => 'AES Encryption', - ); - - const INTERNAL_ATTR_DEFAULT = 0; - const EXTERNAL_ATTR_DEFAULT = 0; - - /* - * Extra field header ID - */ - const EXTID_ZIP64 = 0x0001; // Zip64 - const EXTID_NTFS = 0x000a; // NTFS (for storing full file times information) - const EXTID_UNIX = 0x000d; // UNIX - const EXTID_EXTT = 0x5455; // Info-ZIP Extended Timestamp - const EXTID_UNICODE_FILENAME = 0x7075; // for Unicode filenames - const EXTID_UNICODE_ = 0x6375; // for Unicode file comments - const EXTID_STORING_STRINGS = 0x5A4C; // for storing strings code pages and Unicode filenames using custom Unicode implementation (see Unicode Support: Using Non-English Characters in Filenames, Comments and Passwords). - const EXTID_OFFSETS_COMPRESS_DATA = 0x5A4D; // for saving offsets array from seekable compressed data - const EXTID_AES_ENCRYPTION = 0x9901; // WinZip AES encryption (http://www.winzip.com/aes_info.htm) - - /** - * entry name - * @var string - */ - private $name; - /** - * version made by - * @var int - */ - private $versionMadeBy = self::MADE_BY_WINDOWS_NTFS; - /** - * version needed to extract - * @var int - */ - private $versionExtract = self::EXTRACT_VERSION_20; - /** - * general purpose bit flag - * @var int - */ - private $flag = 0; - /** - * compression method - * @var int - */ - private $compressionMethod = self::COMPRESS_METHOD_DEFLATED; - /** - * last mod file datetime - * @var int Unix timestamp - */ - private $lastModDateTime; - /** - * crc-32 - * @var int - */ - private $crc32; - /** - * compressed size - * @var int - */ - private $compressedSize; - /** - * uncompressed size - * @var int - */ - private $unCompressedSize; - /** - * disk number start - * @var int - */ - private $diskNumber = 0; - /** - * internal file attributes - * @var int - */ - private $internalAttributes = self::INTERNAL_ATTR_DEFAULT; - /** - * external file attributes - * @var int - */ - private $externalAttributes = self::EXTERNAL_ATTR_DEFAULT; - /** - * relative offset of local header - * @var int - */ - private $offsetOfLocal; - /** - * @var int - */ - private $offsetOfCentral; - - /** - * optional extra field data for entry - * - * @var string - */ - private $extraCentral = ""; - /** - * @var string - */ - private $extraLocal = ""; - /** - * optional comment string for entry - * - * @var string - */ - private $comment = ""; - - function __construct() - { - - } - - public function getLengthOfLocal() - { - return $this->getLengthLocalHeader() + $this->compressedSize + ($this->hasDataDescriptor() ? 12 : 0); - } - - public function getLengthLocalHeader() - { - return 30 + strlen($this->name) + strlen($this->extraLocal); - } - - public function getLengthOfCentral() - { - return 46 + strlen($this->name) + strlen($this->extraCentral) + strlen($this->comment); - } - - /** - * @param Buffer $buffer - * @throws ZipException - */ - public function readCentralHeader(Buffer $buffer) - { - $signature = $buffer->getUnsignedInt(); // after offset 4 - if ($signature !== ZipFile::SIGNATURE_CENTRAL_DIR) { - throw new ZipException("Can not read central directory. Bad signature: " . $signature); - } - $this->versionMadeBy = $buffer->getUnsignedShort(); // after offset 6 - $this->versionExtract = $buffer->getUnsignedShort(); // after offset 8 - $this->flag = $buffer->getUnsignedShort(); // after offset 10 - $this->compressionMethod = $buffer->getUnsignedShort(); // after offset 12 - $lastModTime = $buffer->getUnsignedShort(); // after offset 14 - $lastModDate = $buffer->getUnsignedShort(); // after offset 16 - $this->setLastModifyDosDatetime($lastModTime, $lastModDate); - $this->crc32 = $buffer->getUnsignedInt(); // after offset 20 - $this->compressedSize = $buffer->getUnsignedInt(); // after offset 24 - $this->unCompressedSize = $buffer->getUnsignedInt(); // after offset 28 - $fileNameLength = $buffer->getUnsignedShort(); // after offset 30 - $extraCentralLength = $buffer->getUnsignedShort(); // after offset 32 - $fileCommentLength = $buffer->getUnsignedShort(); // after offset 34 - $this->diskNumber = $buffer->getUnsignedShort(); // after offset 36 - $this->internalAttributes = $buffer->getUnsignedShort(); // after offset 38 - $this->externalAttributes = $buffer->getUnsignedInt(); // after offset 42 - $this->offsetOfLocal = $buffer->getUnsignedInt(); // after offset 46 - $this->name = $buffer->getString($fileNameLength); - $this->setExtra($buffer->getString($extraCentralLength)); - $this->comment = $buffer->getString($fileCommentLength); - - $currentPos = $buffer->position(); - $buffer->setPosition($this->offsetOfLocal + 28); - $extraLocalLength = $buffer->getUnsignedShort(); - $buffer->skip($fileNameLength); - $this->extraLocal = $buffer->getString($extraLocalLength); - $buffer->setPosition($currentPos); - } - - /** - * Sets the optional extra field data for the entry. - * - * @param string $extra the extra field data bytes - * @throws ZipException - */ - private function setExtra($extra) - { - if (!empty($extra)) { - $len = strlen($extra); - if ($len > 0xFFFF) { - throw new ZipException("invalid extra field length"); - } - $buffer = new StringBuffer($extra); - $buffer->setOrder(Buffer::LITTLE_ENDIAN); - // extra fields are in "HeaderID(2)DataSize(2)Data... format - while ($buffer->position() + 4 < $len) { - $tag = $buffer->getUnsignedShort(); - $sz = $buffer->getUnsignedShort(); - if ($buffer->position() + $sz > $len) // invalid data - break; - switch ($tag) { - case self::EXTID_ZIP64: - // not support zip64 - break; - case self::EXTID_NTFS: - $buffer->skip(4); // reserved 4 bytes - if ($buffer->getUnsignedShort() != 0x0001 || $buffer->getUnsignedShort() != 24) - break; -// $mtime = winTimeToFileTime($buffer->getLong()); -// $atime = winTimeToFileTime($buffer->getLong()); -// $ctime = winTimeToFileTime($buffer->getLong()); - break; - case self::EXTID_EXTT: - $flag = $buffer->getUnsignedByte(); - $sz0 = 1; - // The CEN-header extra field contains the modification - // time only, or no timestamp at all. 'sz' is used to - // flag its presence or absence. But if mtime is present - // in LOC it must be present in CEN as well. - if (($flag & 0x1) != 0 && ($sz0 + 4) <= $sz) { - $mtime = $buffer->getUnsignedInt(); - $sz0 += 4; - } - if (($flag & 0x2) != 0 && ($sz0 + 4) <= $sz) { - $atime = $buffer->getUnsignedInt(); - $sz0 += 4; - } - if (($flag & 0x4) != 0 && ($sz0 + 4) <= $sz) { - $ctime = $buffer->getUnsignedInt(); - $sz0 += 4; - } - break; - default: - } - } - } - $this->extraCentral = $extra; - } - - /** - * @return Buffer - */ - public function writeLocalHeader() - { - $buffer = new StringBuffer(); - $buffer->setOrder(Buffer::LITTLE_ENDIAN); - $buffer->insertInt(ZipFile::SIGNATURE_LOCAL_HEADER); - $buffer->insertShort($this->versionExtract); - $buffer->insertShort($this->flag); - $buffer->insertShort($this->compressionMethod); - $buffer->insertShort($this->getLastModifyDosTime()); - $buffer->insertShort($this->getLastModifyDosDate()); - if ($this->hasDataDescriptor()) { - $buffer->insertInt(0); - $buffer->insertInt(0); - $buffer->insertInt(0); - } else { - $buffer->insertInt($this->crc32); - $buffer->insertInt($this->compressedSize); - $buffer->insertInt($this->unCompressedSize); - } - $buffer->insertShort(strlen($this->name)); - $buffer->insertShort(strlen($this->extraLocal)); // offset 30 - $buffer->insertString($this->name); - $buffer->insertString($this->extraLocal); - return $buffer; - } - - /** - * @param int $bit - * @return bool - */ - public function setFlagBit($bit) - { - if ($bit < 0 || $bit > 15) { - return false; - } - $this->flag |= 1 << $bit; - return true; - } - - /** - * @param int $bit - * @return bool - */ - public function testFlagBit($bit) - { - return (($this->flag & (1 << $bit)) !== 0); - } - - /** - * @return bool - */ - public function hasDataDescriptor() - { - return $this->testFlagBit(self::FLAG_DATA_DESCRIPTION); - } - - /** - * @return bool - */ - public function isEncrypted() - { - return $this->testFlagBit(self::FLAG_ENCRYPTION); - } - - public function writeDataDescriptor() - { - $buffer = new StringBuffer(); - $buffer->setOrder(Buffer::LITTLE_ENDIAN); - $buffer->insertInt($this->crc32); - $buffer->insertInt($this->compressedSize); - $buffer->insertInt($this->unCompressedSize); - return $buffer; - } - - /** - * @return Buffer - * @throws ZipException - */ - public function writeCentralHeader() - { - $buffer = new StringBuffer(); - $buffer->setOrder(Buffer::LITTLE_ENDIAN); - - $buffer->insertInt(ZipFile::SIGNATURE_CENTRAL_DIR); - $buffer->insertShort($this->versionMadeBy); - $buffer->insertShort($this->versionExtract); - $buffer->insertShort($this->flag); - $buffer->insertShort($this->compressionMethod); - $buffer->insertShort($this->getLastModifyDosTime()); - $buffer->insertShort($this->getLastModifyDosDate()); - $buffer->insertInt($this->crc32); - $buffer->insertInt($this->compressedSize); - $buffer->insertInt($this->unCompressedSize); - $buffer->insertShort(strlen($this->name)); - $buffer->insertShort(strlen($this->extraCentral)); - $buffer->insertShort(strlen($this->comment)); - $buffer->insertShort($this->diskNumber); - $buffer->insertShort($this->internalAttributes); - $buffer->insertInt($this->externalAttributes); - $buffer->insertInt($this->offsetOfLocal); - $buffer->insertString($this->name); - $buffer->insertString($this->extraCentral); - $buffer->insertString($this->comment); - return $buffer; - } - - /** - * @return bool - */ - public function isDirectory() - { - return $this->name[strlen($this->name) - 1] === "/"; - } - - /** - * @return array - */ - public static function getValuesMadeBy() - { - return self::$valuesMadeBy; - } - - /** - * @param array $valuesMadeBy - */ - public static function setValuesMadeBy($valuesMadeBy) - { - self::$valuesMadeBy = $valuesMadeBy; - } - - /** - * @return array - */ - public static function getValuesFlag() - { - return self::$valuesFlag; - } - - /** - * @param array $valuesFlag - */ - public static function setValuesFlag($valuesFlag) - { - self::$valuesFlag = $valuesFlag; - } - - /** - * @return array - */ - public static function getValuesCompressionMethod() - { - return self::$valuesCompressionMethod; - } - - /** - * @param array $valuesCompressionMethod - */ - public static function setValuesCompressionMethod($valuesCompressionMethod) - { - self::$valuesCompressionMethod = $valuesCompressionMethod; - } - - /** - * @return string - */ - public function getName() - { - return $this->name; - } - - /** - * @param string $name - * @throws ZipException - */ - public function setName($name) - { - if (strlen($name) > 0xFFFF) { - throw new ZipException("entry name too long"); - } - $this->name = $name; - $encoding = mb_detect_encoding($this->name, "ASCII, UTF-8", true); - if ($encoding === 'UTF-8') { - $this->setFlagBit(self::FLAG_UTF8); - } - } - - /** - * @return int - */ - public function getVersionMadeBy() - { - return $this->versionMadeBy; - } - - /** - * @param int $versionMadeBy - */ - public function setVersionMadeBy($versionMadeBy) - { - $this->versionMadeBy = $versionMadeBy; - } - - /** - * @return int - */ - public function getVersionExtract() - { - return $this->versionExtract; - } - - /** - * @param int $versionExtract - */ - public function setVersionExtract($versionExtract) - { - $this->versionExtract = $versionExtract; - } - - /** - * @return int - */ - public function getFlag() - { - return $this->flag; - } - - /** - * @param int $flag - */ - public function setFlag($flag) - { - $this->flag = $flag; - } - - /** - * @return int - */ - public function getCompressionMethod() - { - return $this->compressionMethod; - } - - /** - * @param int $compressionMethod - * @throws ZipException - */ - public function setCompressionMethod($compressionMethod) - { - if (!isset(self::$valuesCompressionMethod[$compressionMethod])) { - throw new ZipException("invalid compression method " . $compressionMethod); - } - $this->compressionMethod = $compressionMethod; - } - - /** - * @return int - */ - public function getLastModDateTime() - { - return $this->lastModDateTime; - } - - /** - * @param int $lastModDateTime - */ - public function setLastModDateTime($lastModDateTime) - { - $this->lastModDateTime = $lastModDateTime; - } - - /** - * @return int - */ - public function getCrc32() - { - return $this->crc32; - } - - /** - * @param int $crc32 - * @throws ZipException - */ - public function setCrc32($crc32) - { - if ($crc32 < 0 || $crc32 > 0xFFFFFFFF) { - throw new ZipException("invalid entry crc-32"); - } - $this->crc32 = $crc32; - } - - /** - * @return int - */ - public function getCompressedSize() - { - return $this->compressedSize; - } - - /** - * @param int $compressedSize - */ - public function setCompressedSize($compressedSize) - { - $this->compressedSize = $compressedSize; - } - - /** - * @return int - */ - public function getUnCompressedSize() - { - return $this->unCompressedSize; - } - - /** - * @param int $unCompressedSize - * @throws ZipException - */ - public function setUnCompressedSize($unCompressedSize) - { - if ($unCompressedSize < 0 || $unCompressedSize > 0xFFFFFFFF) { - throw new ZipException("invalid entry size"); - } - $this->unCompressedSize = $unCompressedSize; - } - - /** - * @return int - */ - public function getDiskNumber() - { - return $this->diskNumber; - } - - /** - * @param int $diskNumber - */ - public function setDiskNumber($diskNumber) - { - $this->diskNumber = $diskNumber; - } - - /** - * @return int - */ - public function getInternalAttributes() - { - return $this->internalAttributes; - } - - /** - * @param int $internalAttributes - */ - public function setInternalAttributes($internalAttributes) - { - $this->internalAttributes = $internalAttributes; - } - - /** - * @return int - */ - public function getExternalAttributes() - { - return $this->externalAttributes; - } - - /** - * @param int $externalAttributes - */ - public function setExternalAttributes($externalAttributes) - { - $this->externalAttributes = $externalAttributes; - } - - /** - * @return int - */ - public function getOffsetOfLocal() - { - return $this->offsetOfLocal; - } - - /** - * @param int $offsetOfLocal - */ - public function setOffsetOfLocal($offsetOfLocal) - { - $this->offsetOfLocal = $offsetOfLocal; - } - - /** - * @return int - */ - public function getOffsetOfCentral() - { - return $this->offsetOfCentral; - } - - /** - * @param int $offsetOfCentral - */ - public function setOffsetOfCentral($offsetOfCentral) - { - $this->offsetOfCentral = $offsetOfCentral; - } - - /** - * @return string - */ - public function getExtraCentral() - { - return $this->extraCentral; - } - - /** - * @param string $extra - * @throws ZipException - */ - public function setExtraCentral($extra) - { - if ($extra !== null && strlen($extra) > 0xFFFF) { - throw new ZipException("invalid extra field length"); - } - $this->extraCentral = $extra; - } - - /** - * @param string $extra - * @throws ZipException - */ - public function setExtraLocal($extra) - { - if ($extra !== null && strlen($extra) > 0xFFFF) { - throw new ZipException("invalid extra field length"); - } - $this->extraLocal = $extra; - } - - /** - * @return string - */ - public function getExtraLocal() - { - return $this->extraLocal; - } - - /** - * @return string - */ - public function getComment() - { - return $this->comment; - } - - /** - * @param string $comment - */ - public function setComment($comment) - { - $this->comment = $comment; - } - - /** - * @param int $lastModTime - * @param int $lastModDate - */ - private function setLastModifyDosDatetime($lastModTime, $lastModDate) - { - $hour = ($lastModTime & 0xF800) >> 11; - $minute = ($lastModTime & 0x07E0) >> 5; - $seconds = ($lastModTime & 0x001F) * 2; - - $year = (($lastModDate & 0xFE00) >> 9) + 1980; - $month = ($lastModDate & 0x01E0) >> 5; - $day = $lastModDate & 0x001F; - - // ----- Get UNIX date format - $this->lastModDateTime = mktime($hour, $minute, $seconds, $month, $day, $year); - } - - public function getLastModifyDosTime() - { - $date = getdate($this->lastModDateTime); - return ($date['hours'] << 11) + ($date['minutes'] << 5) + $date['seconds'] / 2; - } - - public function getLastModifyDosDate() - { - $date = getdate($this->lastModDateTime); - return (($date['year'] - 1980) << 9) + ($date['mon'] << 5) + $date['mday']; - } - - public function versionMadeToString() - { - if (isset(self::$valuesMadeBy[$this->versionMadeBy])) { - return self::$valuesMadeBy[$this->versionMadeBy]; - } else return "unknown"; - } - - public function compressionMethodToString() - { - if (isset(self::$valuesCompressionMethod[$this->compressionMethod])) { - return self::$valuesCompressionMethod[$this->compressionMethod]; - } else return "unknown"; - } - - public function flagToString() - { - $return = array(); - foreach (self::$valuesFlag AS $bit => $value) { - if ($this->testFlagBit($bit)) { - $return[] = $value; - } - } - if (!empty($return)) { - return implode(', ', $return); - } else if ($this->flag === 0) { - return "default"; - } - return "unknown"; - } - - function __toString() - { - return __CLASS__ . '{' . - 'name="' . $this->name . '"' . - ', versionMadeBy={' . $this->versionMadeBy . ' => "' . $this->versionMadeToString() . '"}' . - ', versionExtract="' . $this->versionExtract . '"' . - ', flag={' . $this->flag . ' => ' . $this->flagToString() . '}' . - ', compressionMethod={' . $this->compressionMethod . ' => ' . $this->compressionMethodToString() . '}' . - ', lastModify=' . date("Y-m-d H:i:s", $this->lastModDateTime) . - ', crc32=0x' . dechex($this->crc32) . - ', compressedSize=' . ZipUtils::humanSize($this->compressedSize) . - ', unCompressedSize=' . ZipUtils::humanSize($this->unCompressedSize) . - ', diskNumber=' . $this->diskNumber . - ', internalAttributes=' . $this->internalAttributes . - ', externalAttributes=' . $this->externalAttributes . - ', offsetOfLocal=' . $this->offsetOfLocal . - ', offsetOfCentral=' . $this->offsetOfCentral . - ', extraCentral="' . $this->extraCentral . '"' . - ', extraLocal="' . $this->extraLocal . '"' . - ', comment="' . $this->comment . '"' . - '}'; - } -} \ No newline at end of file diff --git a/src/ZipException.php b/src/ZipException.php deleted file mode 100644 index 8beaacb..0000000 --- a/src/ZipException.php +++ /dev/null @@ -1,7 +0,0 @@ -filename = null; - $this->zipEntries = array(); - $this->zipEntriesIndex = array(); - $this->zipComment = ""; - $this->offsetCentralDirectory = 0; - $this->sizeCentralDirectory = 0; - - $this->buffer = new MemoryResourceBuffer(); - $this->buffer->setOrder(Buffer::LITTLE_ENDIAN); - $this->buffer->insertInt(self::SIGNATURE_END_CENTRAL_DIR); - $this->buffer->insertString(str_repeat("\0", 18)); - } - - /** - * Open exists zip archive - * - * @param string $filename - * @throws ZipException - */ - public function open($filename) - { - if (!file_exists($filename)) { - throw new ZipException("Can not open file"); - } - $this->filename = $filename; - $this->openFromString(file_get_contents($this->filename)); - } - - public function openFromString($string) - { - $this->zipEntries = null; - $this->zipEntriesIndex = null; - $this->zipComment = ""; - $this->offsetCentralDirectory = null; - $this->sizeCentralDirectory = 0; - $this->password = null; - - $this->buffer = new StringBuffer($string); - $this->buffer->setOrder(Buffer::LITTLE_ENDIAN); - - $this->findAndReadEndCentralDirectory(); - } - - /** - * Set password - * - * @param string $password - */ - public function setPassword($password) - { - $this->password = $password; - } - - /** - * Find end central catalog - * - * @throws BufferException - * @throws ZipException - */ - private function findAndReadEndCentralDirectory() - { - if ($this->buffer->size() < 26) { - return; - } - $this->buffer->setPosition($this->buffer->size() - 22); - - $endOfCentralDirSignature = $this->buffer->getUnsignedInt(); - if ($endOfCentralDirSignature === self::SIGNATURE_END_CENTRAL_DIR) { - $this->readEndCentralDirectory(); - } else { - $maximumSize = 65557; - if ($this->buffer->size() < $maximumSize) { - $maximumSize = $this->buffer->size(); - } - $this->buffer->skip(-$maximumSize); - $bytes = 0x00000000; - while ($this->buffer->hasRemaining()) { - $byte = $this->buffer->getUnsignedByte(); - $bytes = (($bytes & 0xFFFFFF) << 8) | $byte; - - if ($bytes === 0x504b0506) { - $this->readEndCentralDirectory(); - return; - } - } - throw new ZipException("Unable to find End of Central Dir Record signature"); - } - } - - /** - * Read end central catalog - * - * @throws BufferException - * @throws ZipException - */ - private function readEndCentralDirectory() - { - $this->buffer->skip(4); // number of this disk AND number of the disk with the start of the central directory - $countFiles = $this->buffer->getUnsignedShort(); - $this->buffer->skip(2); // total number of entries in the central directory - $this->sizeCentralDirectory = $this->buffer->getUnsignedInt(); - $this->offsetCentralDirectory = $this->buffer->getUnsignedInt(); - $zipCommentLength = $this->buffer->getUnsignedShort(); - $this->zipComment = $this->buffer->getString($zipCommentLength); - - $this->buffer->setPosition($this->offsetCentralDirectory); - - $this->zipEntries = array(); - $this->zipEntriesIndex = array(); - - for ($i = 0; $i < $countFiles; $i++) { - $offsetOfCentral = $this->buffer->position() - $this->offsetCentralDirectory; - - $zipEntry = new ZipEntry(); - $zipEntry->readCentralHeader($this->buffer); - $zipEntry->setOffsetOfCentral($offsetOfCentral); - - $this->zipEntries[$i] = $zipEntry; - $this->zipEntriesIndex[$zipEntry->getName()] = $i; - } - } - - /** - * @return int - */ - public function getCountFiles() - { - return $this->zipEntries === null ? 0 : sizeof($this->zipEntries); - } - - /** - * Add empty directory in zip archive - * - * @param string $dirName - * @return bool - * @throws ZipException - */ - public function addEmptyDir($dirName) - { - if ($dirName === null) { - throw new ZipException("dirName null"); - } - $dirName = rtrim($dirName, '/') . '/'; - if (isset($this->zipEntriesIndex[$dirName])) { - return true; - } - $zipEntry = new ZipEntry(); - $zipEntry->setName($dirName); - $zipEntry->setCompressionMethod(0); - $zipEntry->setLastModDateTime(time()); - $zipEntry->setCrc32(0); - $zipEntry->setCompressedSize(0); - $zipEntry->setUnCompressedSize(0); - $zipEntry->setOffsetOfLocal($this->offsetCentralDirectory); - - $this->buffer->setPosition($zipEntry->getOffsetOfLocal()); - $bufferLocal = $zipEntry->writeLocalHeader(); - $this->buffer->insert($bufferLocal); - $this->offsetCentralDirectory += $bufferLocal->size(); - - $zipEntry->setOffsetOfCentral($this->sizeCentralDirectory); - $this->buffer->setPosition($this->offsetCentralDirectory + $zipEntry->getOffsetOfCentral()); - $bufferCentral = $zipEntry->writeCentralHeader(); - $this->buffer->insert($bufferCentral); - $this->sizeCentralDirectory += $bufferCentral->size(); - - $this->zipEntries[] = $zipEntry; - end($this->zipEntries); - $this->zipEntriesIndex[$zipEntry->getName()] = key($this->zipEntries); - - $size = $this->getCountFiles(); - $this->buffer->setPosition($this->offsetCentralDirectory + $this->sizeCentralDirectory + 8); -// $signature = $this->buffer->getUnsignedInt(); -// if ($signature !== self::SIGNATURE_END_CENTRAL_DIR) { -// throw new ZipException("error position end central dir"); -// } -// $this->buffer->skip(4); - $this->buffer->putShort($size); - $this->buffer->putShort($size); - $this->buffer->putInt($this->sizeCentralDirectory); - $this->buffer->putInt($this->offsetCentralDirectory); - return true; - } - - /** - * @param string $inDirectory - * @param string|null $addPath - * @param array $ignoreFiles - * @return bool - * @throws ZipException - */ - public function addDir($inDirectory, $addPath = null, array $ignoreFiles = array()) - { - if ($inDirectory === null) { - throw new ZipException("dirName null"); - } - if (!file_exists($inDirectory)) { - throw new ZipException("directory not found"); - } - if (!is_dir($inDirectory)) { - throw new ZipException("input directory is not directory"); - } - if ($addPath !== null && is_string($addPath) && !empty($addPath)) { - $addPath = rtrim($addPath, '/'); - } else { - $addPath = ""; - } - $inDirectory = rtrim($inDirectory, '/'); - - $iterator = new FilterFileIterator(new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($inDirectory)), $ignoreFiles); - $files = iterator_to_array($iterator, false); - - $count = $this->getCountFiles(); - /** - * @var \SplFileInfo $file - */ - foreach ($files as $file) { - if ($file->getFilename() === '.') { - $filename = dirname(str_replace($inDirectory, $addPath, $file)); - $this->isEmptyDir($file) && $this->addEmptyDir($filename); - } else if ($file->isFile()) { - $filename = str_replace($inDirectory, $addPath, $file); - $this->addFile($file, $filename); - } - } - return $this->getCountFiles() > $count; - } - - public function addGlob($pattern, $removePath = null, $addPath = null, $recursive = true) - { - if ($pattern === null) { - throw new ZipException("pattern null"); - } - $glob = $this->globFileSearch($pattern, GLOB_BRACE, $recursive); - if ($glob === FALSE || empty($glob)) { - return false; - } - if (!empty($addPath) && is_string($addPath)) { - $addPath = rtrim($addPath, '/'); - } else { - $addPath = ""; - } - if (!empty($removePath) && is_string($removePath)) { - $removePath = rtrim($removePath, '/'); - } else { - $removePath = ""; - } - - $count = $this->getCountFiles(); - /** - * @var string $file - */ - foreach ($glob as $file) { - if (is_dir($file)) { - $filename = str_replace($addPath, $removePath, $file); - $this->isEmptyDir($file) && $this->addEmptyDir($filename); - } else if (is_file($file)) { - $filename = str_replace($removePath, $addPath, $file); - $this->addFile($file, $filename); - } - } - return $this->getCountFiles() > $count; - } - - public function addPattern($pattern, $inDirectory, $addPath = null, $recursive = true) - { - if ($pattern === null) { - throw new ZipException("pattern null"); - } - $files = $this->regexFileSearch($inDirectory, $pattern, $recursive); - if ($files === FALSE || empty($files)) { - return false; - } - if (!empty($addPath) && is_string($addPath)) { - $addPath = rtrim($addPath, '/'); - } else { - $addPath = ""; - } - $inDirectory = rtrim($inDirectory, '/'); - - $count = $this->getCountFiles(); - /** - * @var string $file - */ - foreach ($files as $file) { - if (is_dir($file)) { - $filename = str_replace($addPath, $inDirectory, $file); - $this->isEmptyDir($file) && $this->addEmptyDir($filename); - } else if (is_file($file)) { - $filename = str_replace($inDirectory, $addPath, $file); - $this->addFile($file, $filename); - } - } - return $this->getCountFiles() > $count; - } - - private function globFileSearch($pattern, $flags = 0, $recursive = true) - { - $files = glob($pattern, $flags); - if (!$recursive) return $files; - foreach (glob(dirname($pattern) . '/*', GLOB_ONLYDIR | GLOB_NOSORT) as $dir) { - $files = array_merge($files, $this->globFileSearch($dir . '/' . basename($pattern), $flags, $recursive)); - } - return $files; - } - - private function regexFileSearch($folder, $pattern, $recursive = true) - { - $dir = $recursive ? new \RecursiveDirectoryIterator($folder) : new \DirectoryIterator($folder); - $ite = $recursive ? new \RecursiveIteratorIterator($dir) : new \IteratorIterator($dir); - $files = new \RegexIterator($ite, $pattern, \RegexIterator::GET_MATCH); - $fileList = array(); - foreach ($files as $file) { - $fileList = array_merge($fileList, $file); - } - return $fileList; - } - - private function isEmptyDir($dir) - { - if (!is_readable($dir)) return false; - return (count(scandir($dir)) == 2); - } - - /** - * Add file in zip archive - * - * @param string $filename - * @param string|null $localName - * @param int|null $compressionMethod - * @throws ZipException - */ - public function addFile($filename, $localName = NULL, $compressionMethod = null) - { - if ($filename === null) { - throw new ZipException("filename null"); - } - if (!file_exists($filename)) { - throw new ZipException("file not found"); - } - if (!is_file($filename)) { - throw new ZipException("input filename is not file"); - } - if ($localName === null) { - $localName = basename($filename); - } - $this->addFromString($localName, file_get_contents($filename), $compressionMethod); - } - - /** - * @param string $localName - * @param string $contents - * @param int|null $compressionMethod - * @throws ZipException - */ - public function addFromString($localName, $contents, $compressionMethod = null) - { - if ($localName === null || !is_string($localName) || strlen($localName) === 0) { - throw new ZipException("local name empty"); - } - if ($contents === null) { - throw new ZipException("contents null"); - } - $unCompressedSize = strlen($contents); - $compress = null; - if ($compressionMethod === null) { - if ($unCompressedSize === 0) { - $compressionMethod = ZipEntry::COMPRESS_METHOD_STORED; - } else { - $compressionMethod = ZipEntry::COMPRESS_METHOD_DEFLATED; - } - } - switch ($compressionMethod) { - case ZipEntry::COMPRESS_METHOD_STORED: - $compress = $contents; - break; - case ZipEntry::COMPRESS_METHOD_DEFLATED: - $compress = gzdeflate($contents); - break; - default: - throw new ZipException("Compression method not support"); - } - $crc32 = sprintf('%u', crc32($contents)); - $compressedSize = strlen($compress); - - if (isset($this->zipEntriesIndex[$localName])) { - /** - * @var int $index - */ - $index = $this->zipEntriesIndex[$localName]; - $zipEntry = &$this->zipEntries[$index]; - - $oldCompressedSize = $zipEntry->getCompressedSize(); - - $zipEntry->setCompressionMethod($compressionMethod); - $zipEntry->setLastModDateTime(time()); - $zipEntry->setCompressedSize($compressedSize); - $zipEntry->setUnCompressedSize($unCompressedSize); - $zipEntry->setCrc32($crc32); - - $this->buffer->setPosition($zipEntry->getOffsetOfLocal() + 8); - $this->buffer->putShort($zipEntry->getCompressionMethod()); - $this->buffer->putShort($zipEntry->getLastModifyDosTime()); - $this->buffer->putShort($zipEntry->getLastModifyDosDate()); - if ($zipEntry->hasDataDescriptor()) { - $this->buffer->skip(12); - } else { - $this->buffer->putInt($zipEntry->getCrc32()); - $this->buffer->putInt($zipEntry->getCompressedSize()); - $this->buffer->putInt($zipEntry->getUnCompressedSize()); - } - $this->buffer->skip(4 + strlen($zipEntry->getName()) + strlen($zipEntry->getExtraLocal())); - $this->buffer->replaceString($compress, $oldCompressedSize); - - if ($zipEntry->hasDataDescriptor()) { - $this->buffer->put($zipEntry->writeDataDescriptor()); - } - - $diff = $oldCompressedSize - $zipEntry->getCompressedSize(); - if ($diff !== 0) { - $this->offsetCentralDirectory -= $diff; - } - $this->buffer->setPosition($this->offsetCentralDirectory + $zipEntry->getOffsetOfCentral() + 10); - $this->buffer->putShort($zipEntry->getCompressionMethod()); - $this->buffer->putShort($zipEntry->getLastModifyDosTime()); - $this->buffer->putShort($zipEntry->getLastModifyDosDate()); - $this->buffer->putInt($zipEntry->getCrc32()); - $this->buffer->putInt($zipEntry->getCompressedSize()); - $this->buffer->putInt($zipEntry->getUnCompressedSize()); - - if ($diff !== 0) { - $this->buffer->skip(18 + strlen($zipEntry->getName()) + strlen($zipEntry->getExtraCentral()) + strlen($zipEntry->getComment())); - - $size = $this->getCountFiles(); - /** - * @var ZipEntry $entry - */ - for ($i = $index + 1; $i < $size; $i++) { - $zipEntry = &$this->zipEntries[$i]; - - $zipEntry->setOffsetOfLocal($zipEntry->getOffsetOfLocal() - $diff); - $this->buffer->setPosition($this->offsetCentralDirectory + $zipEntry->getOffsetOfCentral() + 42); -// $this->buffer->setPosition($this->offsetCentralDirectory + $zipEntry->getOffsetOfCentral()); -// $sig = $this->buffer->getUnsignedInt(); -// if ($sig !== self::SIGNATURE_CENTRAL_DIR) { -// $this->buffer->skip(-4); -// throw new ZipException("Signature central dir corrupt. Bad signature = 0x" . dechex($sig) . "; Current entry: " . $entry->getName()); -// } -// $this->buffer->skip(38); - $this->buffer->putInt($zipEntry->getOffsetOfLocal()); - } - - $this->buffer->setPosition($this->offsetCentralDirectory + $this->sizeCentralDirectory + 12); -// $signature = $this->buffer->getUnsignedInt(); -// if ($signature !== self::SIGNATURE_END_CENTRAL_DIR) { -// throw new ZipException("error position end central dir"); -// } -// $this->buffer->skip(8); - $this->buffer->putInt($this->sizeCentralDirectory); - $this->buffer->putInt($this->offsetCentralDirectory); - } - } else { - $zipEntry = new ZipEntry(); -// if ($flagBit > 0) $zipEntry->setFlagBit($flagBit); - $zipEntry->setName($localName); - $zipEntry->setCompressionMethod($compressionMethod); - $zipEntry->setLastModDateTime(time()); - $zipEntry->setCrc32($crc32); - $zipEntry->setCompressedSize($compressedSize); - $zipEntry->setUnCompressedSize($unCompressedSize); - $zipEntry->setOffsetOfLocal($this->offsetCentralDirectory); - - $bufferLocal = $zipEntry->writeLocalHeader(); - $bufferLocal->insertString($compress); - if ($zipEntry->hasDataDescriptor()) { - $bufferLocal->insert($zipEntry->writeDataDescriptor()); - } - - $this->buffer->setPosition($zipEntry->getOffsetOfLocal()); - $this->buffer->insert($bufferLocal); - $this->offsetCentralDirectory += $bufferLocal->size(); - - $zipEntry->setOffsetOfCentral($this->sizeCentralDirectory); - $this->buffer->setPosition($this->offsetCentralDirectory + $zipEntry->getOffsetOfCentral()); - $bufferCentral = $zipEntry->writeCentralHeader(); - $this->buffer->insert($bufferCentral); - $this->sizeCentralDirectory += $bufferCentral->size(); - - $this->zipEntries[] = $zipEntry; - end($this->zipEntries); - $this->zipEntriesIndex[$zipEntry->getName()] = key($this->zipEntries); - - $size = $this->getCountFiles(); - - $this->buffer->setPosition($this->offsetCentralDirectory + $this->sizeCentralDirectory + 8); -// $signature = $this->buffer->getUnsignedInt(); -// if ($signature !== self::SIGNATURE_END_CENTRAL_DIR) { -// throw new ZipException("error position end central dir"); -// } -// $this->buffer->skip(4); - $this->buffer->putShort($size); - $this->buffer->putShort($size); - $this->buffer->putInt($this->sizeCentralDirectory); - $this->buffer->putInt($this->offsetCentralDirectory); - } - } - - /** - * Update timestamp archive for all files - * - * @param int|null $timestamp - * @throws BufferException - */ - public function updateTimestamp($timestamp = null) - { - if ($timestamp === null || !is_int($timestamp)) { - $timestamp = time(); - } - foreach ($this->zipEntries AS $entry) { - $entry->setLastModDateTime($timestamp); - $this->buffer->setPosition($entry->getOffsetOfLocal() + 10); - $this->buffer->putShort($entry->getLastModifyDosTime()); - $this->buffer->putShort($entry->getLastModifyDosDate()); - - $this->buffer->setPosition($this->offsetCentralDirectory + $entry->getOffsetOfCentral() + 12); - $this->buffer->putShort($entry->getLastModifyDosTime()); - $this->buffer->putShort($entry->getLastModifyDosDate()); - } - } - - public function deleteGlob($pattern) - { - if ($pattern === null) { - throw new ZipException("pattern null"); - } - $pattern = '~' . $this->convertGlobToRegEx($pattern) . '~si'; - return $this->deletePattern($pattern); - } - - public function deletePattern($pattern) - { - if ($pattern === null) { - throw new ZipException("pattern null"); - } - $offsetLocal = 0; - $offsetCentral = 0; - $modify = false; - foreach ($this->zipEntries AS $index => &$entry) { - if (preg_match($pattern, $entry->getName())) { - $this->buffer->setPosition($entry->getOffsetOfLocal() - $offsetLocal); - $lengthLocal = $entry->getLengthOfLocal(); - $this->buffer->remove($lengthLocal); - $offsetLocal += $lengthLocal; - - $this->offsetCentralDirectory -= $lengthLocal; - - $this->buffer->setPosition($this->offsetCentralDirectory + $entry->getOffsetOfCentral() - $offsetCentral); - $lengthCentral = $entry->getLengthOfCentral(); - $this->buffer->remove($lengthCentral); - $offsetCentral += $lengthCentral; - - $this->sizeCentralDirectory -= $lengthCentral; - - unset($this->zipEntries[$index], $this->zipEntriesIndex[$entry->getName()]); - $modify = true; - continue; - } - if ($modify) { - $entry->setOffsetOfLocal($entry->getOffsetOfLocal() - $offsetLocal); - $entry->setOffsetOfCentral($entry->getOffsetOfCentral() - $offsetCentral); - $this->buffer->setPosition($this->offsetCentralDirectory + $entry->getOffsetOfCentral() + 42); - $this->buffer->putInt($entry->getOffsetOfLocal()); - } - } - if ($modify) { - $size = $this->getCountFiles(); - $this->buffer->setPosition($this->offsetCentralDirectory + $this->sizeCentralDirectory + 8); -// $signature = $this->buffer->getUnsignedInt(); -// if ($signature !== self::SIGNATURE_END_CENTRAL_DIR) { -// throw new ZipException("error position end central dir"); -// } -// $this->buffer->skip(4); - $this->buffer->putShort($size); - $this->buffer->putShort($size); - $this->buffer->putInt($this->sizeCentralDirectory); - $this->buffer->putInt($this->offsetCentralDirectory); - return true; - } - return false; - } - - /** - * @param int $index - * @return bool - * @throws ZipException - */ - public function deleteIndex($index) - { - if ($index === null || !is_numeric($index)) { - throw new ZipException("index no numeric"); - } - if (!isset($this->zipEntries[$index])) { - return false; - } - - $entry = $this->zipEntries[$index]; - - $offsetCentral = $entry->getOffsetOfCentral(); - $lengthCentral = $entry->getLengthOfCentral(); - - $offsetLocal = $entry->getOffsetOfLocal(); - $lengthLocal = $entry->getLengthOfLocal(); - - unset( - $this->zipEntries[$index], - $this->zipEntriesIndex[$entry->getName()] - ); - $this->zipEntries = array_values($this->zipEntries); - $this->zipEntriesIndex = array_flip(array_keys($this->zipEntriesIndex)); - - $size = $this->getCountFiles(); - - $this->buffer->setPosition($this->offsetCentralDirectory + $offsetCentral); - $this->buffer->remove($lengthCentral); - - $this->buffer->setPosition($offsetLocal); - $this->buffer->remove($lengthLocal); - - $this->offsetCentralDirectory -= $lengthLocal; - $this->sizeCentralDirectory -= $lengthCentral; - - /** - * @var ZipEntry $entry - */ - for ($i = $index; $i < $size; $i++) { - $entry = &$this->zipEntries[$i]; - - $entry->setOffsetOfLocal($entry->getOffsetOfLocal() - $lengthLocal); -// $this->buffer->setPosition($entry->getOffsetOfLocal()); -// $sig = $this->buffer->getUnsignedInt(); -// if ($sig !== self::SIGNATURE_LOCAL_HEADER) { -// throw new ZipException("Signature local header corrupt"); -// } - $entry->setOffsetOfCentral($entry->getOffsetOfCentral() - $lengthCentral); - - $this->buffer->setPosition($this->offsetCentralDirectory + $entry->getOffsetOfCentral() + 42); -// $this->buffer->setPosition($this->offsetCentralDirectory + $entry->getOffsetOfCentral()); -// $sig = $this->buffer->getUnsignedInt(); -// if ($sig !== self::SIGNATURE_CENTRAL_DIR) { -// $this->buffer->skip(-4); -// throw new ZipException("Signature central dir corrupt. Bad signature = 0x" . dechex($sig) . "; Current entry: " . $entry->getName()); -// } -// $this->buffer->skip(38); - $this->buffer->putInt($entry->getOffsetOfLocal()); - } - - $this->buffer->setPosition($this->offsetCentralDirectory + $this->sizeCentralDirectory + 8); -// $signature = $this->buffer->getUnsignedInt(); -// if ($signature !== self::SIGNATURE_END_CENTRAL_DIR) { -// throw new ZipException("error position end central dir"); -// } -// $this->buffer->skip(4); - $this->buffer->putShort($size); - $this->buffer->putShort($size); - $this->buffer->putInt($this->sizeCentralDirectory); - $this->buffer->putInt($this->offsetCentralDirectory); - return true; - } - - public function deleteAll() - { - $this->zipEntries = array(); - $this->zipEntriesIndex = array(); - $this->offsetCentralDirectory = 0; - $this->sizeCentralDirectory = 0; - - $this->buffer->truncate(); - $this->buffer->insertInt(self::SIGNATURE_END_CENTRAL_DIR); - $this->buffer->insertString(str_repeat("\0", 18)); - } - - /** - * @param $name - * @return bool - * @throws ZipException - */ - public function deleteName($name) - { - if (empty($name)) { - throw new ZipException("name is empty"); - } - if (!isset($this->zipEntriesIndex[$name])) { - return false; - } - $index = $this->zipEntriesIndex[$name]; - return $this->deleteIndex($index); - } - - /** - * @param string $destination - * @param array $entries - * @return bool - * @throws ZipException - */ - public function extractTo($destination, array $entries = null) - { - if ($this->zipEntries === NULL) { - throw new ZipException("zip entries not initial"); - } - if (!file_exists($destination)) { - throw new ZipException("Destination " . $destination . " not found"); - } - if (!is_dir($destination)) { - throw new ZipException("Destination is not directory"); - } - if (!is_writable($destination)) { - throw new ZipException("Destination is not writable directory"); - } - - /** - * @var ZipEntry[] $zipEntries - */ - if ($entries !== null && is_array($entries) && !empty($entries)) { - $flipEntries = array_flip($entries); - $zipEntries = array_filter($this->zipEntries, function ($zipEntry) use ($flipEntries) { - /** - * @var ZipEntry $zipEntry - */ - return isset($flipEntries[$zipEntry->getName()]); - }); - } else { - $zipEntries = $this->zipEntries; - } - - $extract = 0; - foreach ($zipEntries AS $entry) { - $file = $destination . '/' . $entry->getName(); - $dir = dirname($file); - if (!file_exists($dir)) { - if (!mkdir($dir, 0755, true)) { - throw new ZipException("Can not create dir " . $dir); - } - chmod($dir, 0755); - } - if ($entry->isDirectory()) { - continue; - } - if (file_put_contents($file, $this->getEntryBytes($entry)) === FALSE) { - return false; - } - touch($file, $entry->getLastModDateTime()); - $extract++; - } - return $extract > 0; - } - - /** - * @param ZipEntry $entry - * @return string - * @throws BufferException - * @throws ZipException - */ - private function getEntryBytes(ZipEntry $entry) - { - $this->buffer->setPosition($entry->getOffsetOfLocal() + $entry->getLengthLocalHeader()); -// $this->buffer->setPosition($entry->getOffsetOfLocal()); -// $signature = $this->buffer->getUnsignedInt(); -// if ($signature !== self::SIGNATURE_LOCAL_HEADER) { -// throw new ZipException("Can not read entry " . $entry->getName()); -// } -// $this->buffer->skip($entry->getLengthLocalHeader() - 4); - - $string = $this->buffer->getString($entry->getCompressedSize()); - - if ($entry->isEncrypted()) { - if (empty($this->password)) { - throw new ZipException("need password archive"); - } - - $pwdKeys = self::$initPwdKeys; - - $bufPass = new StringBuffer($this->password); - while ($bufPass->hasRemaining()) { - $byte = $bufPass->getUnsignedByte(); - $pwdKeys = ZipUtils::updateKeys($byte, $pwdKeys); - } - unset($bufPass); - - $keys = $pwdKeys; - - $strBuffer = new StringBuffer($string); - for ($i = 0; $i < ZipUtils::DECRYPT_HEADER_SIZE; $i++) { - $result = $strBuffer->getUnsignedByte(); - $lastValue = $result ^ ZipUtils::decryptByte($keys[2]); - $keys = ZipUtils::updateKeys($lastValue, $keys); - } - - $string = ""; - while ($strBuffer->hasRemaining()) { - $result = $strBuffer->getUnsignedByte(); - $result = ($result ^ ZipUtils::decryptByte($keys[2])) & 0xff; - $keys = ZipUtils::updateKeys(MathHelper::castToByte($result), $keys); - $string .= chr($result); - } - unset($strBuffer); - } - - switch ($entry->getCompressionMethod()) { - case ZipEntry::COMPRESS_METHOD_DEFLATED: - $string = @gzinflate($string); - break; - case ZipEntry::COMPRESS_METHOD_STORED: - break; - default: - throw new ZipException("Compression method " . $entry->compressionMethodToString() . " not support!"); - } - $expectedCrc = sprintf('%u', crc32($string)); - if ($expectedCrc != $entry->getCrc32()) { - if ($entry->isEncrypted()) { - throw new ZipException("Wrong password"); - } - throw new ZipException("File " . $entry->getName() . ' corrupt. Bad CRC ' . dechex($expectedCrc) . ' (should be ' . dechex($entry->getCrc32()) . ')'); - } - return $string; - } - - /** - * @return string - */ - public function getArchiveComment() - { - return $this->zipComment; - } - - /** - * @param $index - * @return string - * @throws ZipException - */ - public function getCommentIndex($index) - { - if (!isset($this->zipEntries[$index])) { - throw new ZipException("File for index " . $index . " not found"); - } - return $this->zipEntries[$index]->getComment(); - } - - /** - * @param string $name - * @return string - * @throws ZipException - */ - public function getCommentName($name) - { - if (!isset($this->zipEntriesIndex[$name])) { - throw new ZipException("File for name " . $name . " not found"); - } - $index = $this->zipEntriesIndex[$name]; - return $this->getCommentIndex($index); - } - - /** - * @param int $index - * @return string - * @throws ZipException - */ - public function getFromIndex($index) - { - if (!isset($this->zipEntries[$index])) { - throw new ZipException("File for index " . $index . " not found"); - } - return $this->getEntryBytes($this->zipEntries[$index]); - } - - /** - * @param string $name - * @return string - * @throws ZipException - */ - public function getFromName($name) - { - if (!isset($this->zipEntriesIndex[$name])) { - throw new ZipException("File for name " . $name . " not found"); - } - $index = $this->zipEntriesIndex[$name]; - return $this->getEntryBytes($this->zipEntries[$index]); - } - - /** - * @param int $index - * @return string - * @throws ZipException - */ - public function getNameIndex($index) - { - if (!isset($this->zipEntries[$index])) { - throw new ZipException("File for index " . $index . " not found"); - } - return $this->zipEntries[$index]->getName(); - } - - /** - * @param string $name - * @return bool|string - */ - public function locateName($name) - { - return isset($this->zipEntriesIndex[$name]) ? $this->zipEntriesIndex[$name] : false; - } - - /** - * @param int $index - * @param string $newName - * @return bool - * @throws ZipException - */ - public function renameIndex($index, $newName) - { - if (!isset($this->zipEntries[$index])) { - throw new ZipException("File for index " . $index . " not found"); - } - $lengthNewName = strlen($newName); - if (strlen($lengthNewName) > 0xFF) { - throw new ZipException("Length new name is very long. Maximum size 255"); - } - $entry = &$this->zipEntries[$index]; - if ($entry->getName() === $newName) { - return true; - } - if (isset($this->zipEntriesIndex[$newName])) { - return false; - } - - $lengthOldName = strlen($entry->getName()); - - $this->buffer->setPosition($entry->getOffsetOfLocal() + 26); - $this->buffer->putShort($lengthNewName); - $this->buffer->skip(2); - if ($lengthOldName === $lengthNewName) { - $this->buffer->putString($newName); - $this->buffer->setPosition($this->offsetCentralDirectory + $entry->getOffsetOfCentral() + 46); - $this->buffer->putString($newName); - } else { - $this->buffer->replaceString($newName, $lengthOldName); - $diff = $lengthOldName - $lengthNewName; - - $this->offsetCentralDirectory -= $diff; - - $this->buffer->setPosition($this->offsetCentralDirectory + $entry->getOffsetOfCentral() + 28); - $this->buffer->putShort($lengthNewName); - $this->buffer->skip(16); - $this->buffer->replaceString($newName, $lengthOldName); - $this->sizeCentralDirectory -= $diff; - - $size = $this->getCountFiles(); - for ($i = $index + 1; $i < $size; $i++) { - $zipEntry = &$this->zipEntries[$i]; - $zipEntry->setOffsetOfLocal($zipEntry->getOffsetOfLocal() - $diff); - $zipEntry->setOffsetOfCentral($zipEntry->getOffsetOfCentral() - $diff); - $this->buffer->setPosition($this->offsetCentralDirectory + $zipEntry->getOffsetOfCentral() + 42); -// $this->buffer->setPosition($this->offsetCentralDirectory + $zipEntry->getOffsetOfCentral()); -// $sig = $this->buffer->getUnsignedInt(); -// if ($sig !== self::SIGNATURE_CENTRAL_DIR) { -// $this->buffer->skip(-4); -// throw new ZipException("Signature central dir corrupt. Bad signature = 0x" . dechex($sig) . "; Current entry: " . $entry->getName()); -// } -// $this->buffer->skip(38); - $this->buffer->putInt($zipEntry->getOffsetOfLocal()); - } - - $this->buffer->setPosition($this->offsetCentralDirectory + $this->sizeCentralDirectory + 12); -// $this->buffer->setPosition($this->offsetCentralDirectory + $this->sizeCentralDirectory); -// $signature = $this->buffer->getUnsignedInt(); -// if ($signature !== self::SIGNATURE_END_CENTRAL_DIR) { -// throw new ZipException("error position end central dir"); -// } -// $this->buffer->skip(8); - $this->buffer->putInt($this->sizeCentralDirectory); - $this->buffer->putInt($this->offsetCentralDirectory); - } - $entry->setName($newName); - return true; - } - - /** - * @param string $name - * @param string $newName - * @return bool - * @throws ZipException - */ - public function renameName($name, $newName) - { - if (!isset($this->zipEntriesIndex[$name])) { - throw new ZipException("File for name " . $name . " not found"); - } - $index = $this->zipEntriesIndex[$name]; - return $this->renameIndex($index, $newName); - } - - /** - * @param string $comment - * @return bool - * @throws ZipException - */ - public function setArchiveComment($comment) - { - if ($comment === null) { - return false; - } - if ($comment === $this->zipComment) { - return true; - } - $currentCommentLength = strlen($this->zipComment); - $commentLength = strlen($comment); - if ($commentLength > 0xffff) { - $commentLength = 0xffff; - $comment = substr($comment, 0, $commentLength); - } - - $this->buffer->setPosition($this->offsetCentralDirectory + $this->sizeCentralDirectory + 20); -// $signature = $this->buffer->getUnsignedInt(); -// if ($signature !== self::SIGNATURE_END_CENTRAL_DIR) { -// throw new ZipException("error position end central dir"); -// } -// $this->buffer->skip(16); - $this->buffer->putShort($commentLength); - $this->buffer->replaceString($comment, $currentCommentLength); - - $this->zipComment = $comment; - return true; - } - - /** - * Set the comment of an entry defined by its index - * - * @param int $index - * @param string $comment - * @return bool - * @throws ZipException - */ - public function setCommentIndex($index, $comment) - { - if (!isset($this->zipEntries[$index])) { - throw new ZipException("File for index " . $index . " not found"); - } - if ($comment === null) { - return false; - } - $newCommentLength = strlen($comment); - if ($newCommentLength > 0xffff) { - $newCommentLength = 0xffff; - $comment = substr($comment, 0, $newCommentLength); - } - $entry = &$this->zipEntries[$index]; - $oldComment = $entry->getComment(); - $oldCommentLength = strlen($oldComment); - $this->buffer->setPosition($this->offsetCentralDirectory + $entry->getOffsetOfCentral() + 32); - - $this->buffer->putShort($newCommentLength); - $this->buffer->skip(12 + strlen($entry->getName()) + strlen($entry->getExtraCentral())); - - if ($oldCommentLength === $newCommentLength) { - $this->buffer->putString($comment); - } else { - $this->buffer->replaceString($comment, $oldCommentLength); - $diff = $oldCommentLength - $newCommentLength; - - $this->sizeCentralDirectory -= $diff; - $size = $this->getCountFiles(); - /** - * @var ZipEntry $entry - */ - for ($i = $index + 1; $i < $size; $i++) { - $zipEntry = &$this->zipEntries[$i]; - $zipEntry->setOffsetOfCentral($zipEntry->getOffsetOfCentral() - $diff); - } - - $this->buffer->setPosition($this->offsetCentralDirectory + $this->sizeCentralDirectory + 12); -// $signature = $this->buffer->getUnsignedInt(); -// if ($signature !== self::SIGNATURE_END_CENTRAL_DIR) { -// throw new ZipException("error position end central dir"); -// } -// $this->buffer->skip(8); - $this->buffer->putInt($this->sizeCentralDirectory); - } - $entry->setComment($comment); - return true; - } - - /** - * @return ZipEntry[] - */ - public function getZipEntries() - { - return $this->zipEntries; - } - - /** - * @param int $index - * @return ZipEntry|bool - */ - public function getZipEntryIndex($index) - { - return isset($this->zipEntries[$index]) ? $this->zipEntries[$index] : false; - } - - /** - * @param string $name - * @return ZipEntry|bool - */ - public function getZipEntryName($name) - { - return isset($this->zipEntriesIndex[$name]) ? $this->zipEntries[$this->zipEntriesIndex[$name]] : false; - } - - /** - * Set the comment of an entry defined by its name - * - * @param string $name - * @param string $comment - * @return bool - * @throws ZipException - */ - public function setCommentName($name, $comment) - { - if (!isset($this->zipEntriesIndex[$name])) { - throw new ZipException("File for name " . $name . " not found"); - } - $index = $this->zipEntriesIndex[$name]; - return $this->setCommentIndex($index, $comment); - } - - /** - * @param $index - * @return array - * @throws ZipException - */ - public function statIndex($index) - { - if (!isset($this->zipEntries[$index])) { - throw new ZipException("File for index " . $index . " not found"); - } - $entry = $this->zipEntries[$index]; - return array( - 'name' => $entry->getName(), - 'index' => $index, - 'crc' => $entry->getCrc32(), - 'size' => $entry->getUnCompressedSize(), - 'mtime' => $entry->getLastModDateTime(), - 'comp_size' => $entry->getCompressedSize(), - 'comp_method' => $entry->getCompressionMethod() - ); - } - - /** - * @param string $name - * @return array - * @throws ZipException - */ - public function statName($name) - { - if (!isset($this->zipEntriesIndex[$name])) { - throw new ZipException("File for name " . $name . " not found"); - } - $index = $this->zipEntriesIndex[$name]; - return $this->statIndex($index); - } - - public function getListFiles() - { - return array_flip($this->zipEntriesIndex); - } - - /** - * @return array - */ - public function getExtendedListFiles() - { - - return array_map(function ($index, $entry) { - /** - * @var ZipEntry $entry - * @var int $index - */ - return array( - 'name' => $entry->getName(), - 'index' => $index, - 'crc' => $entry->getCrc32(), - 'size' => $entry->getUnCompressedSize(), - 'mtime' => $entry->getLastModDateTime(), - 'comp_size' => $entry->getUnCompressedSize(), - 'comp_method' => $entry->getCompressionMethod() - ); - }, array_keys($this->zipEntries), $this->zipEntries); - } - - public function output() - { - return $this->buffer->toString(); - } - - /** - * @param string $file - * @return bool - */ - public function saveAs($file) - { - return file_put_contents($file, $this->output()) !== false; - } - - /** - * @return bool - */ - public function save() - { - if ($this->filename !== NULL) { - return file_put_contents($this->filename, $this->output()) !== false; - } - return false; - } - - public function close() - { - if ($this->buffer !== null) { - ($this->buffer instanceof ResourceBuffer) && $this->buffer->close(); - } - $this->zipEntries = null; - $this->zipEntriesIndex = null; - $this->zipComment = null; - $this->buffer = null; - $this->filename = null; - $this->offsetCentralDirectory = null; - } - - function __destruct() - { - $this->close(); - } - - private static function convertGlobToRegEx($pattern) - { - $pattern = trim($pattern, '*'); // Remove beginning and ending * globs because they're useless - $escaping = false; - $inCurlies = 0; - $chars = str_split($pattern); - $sb = ''; - foreach ($chars AS $currentChar) { - switch ($currentChar) { - case '*': - $sb .= ($escaping ? "\\*" : '.*'); - $escaping = false; - break; - case '?': - $sb .= ($escaping ? "\\?" : '.'); - $escaping = false; - break; - case '.': - case '(': - case ')': - case '+': - case '|': - case '^': - case '$': - case '@': - case '%': - $sb .= '\\' . $currentChar; - $escaping = false; - break; - case '\\': - if ($escaping) { - $sb .= "\\\\"; - $escaping = false; - } else { - $escaping = true; - } - break; - case '{': - if ($escaping) { - $sb .= "\\{"; - } else { - $sb = '('; - $inCurlies++; - } - $escaping = false; - break; - case '}': - if ($inCurlies > 0 && !$escaping) { - $sb .= ')'; - $inCurlies--; - } else if ($escaping) - $sb = "\\}"; - else - $sb = "}"; - $escaping = false; - break; - case ',': - if ($inCurlies > 0 && !$escaping) { - $sb .= '|'; - } else if ($escaping) - $sb .= "\\,"; - else - $sb = ","; - break; - default: - $escaping = false; - $sb .= $currentChar; - } - } - return $sb; - } - -} diff --git a/src/ZipUtils.php b/src/ZipUtils.php deleted file mode 100644 index c54b6b9..0000000 --- a/src/ZipUtils.php +++ /dev/null @@ -1,104 +0,0 @@ ->> 8) ^ CRC_TABLE[(oldCrc ^ charAt) & 0xff]) - * - * @param int $oldCrc - * @param int $charAt - * @return int|string - */ - public static function crc32($oldCrc, $charAt) - { - return MathHelper::castToInt(MathHelper::bitwiseXor(MathHelper::unsignedRightShift32($oldCrc, 8), self::$CRC_TABLE[MathHelper::bitwiseAnd(MathHelper::bitwiseXor($oldCrc, $charAt), 0xff)])); - } - - public static function decryptByte($byte) - { - $temp = $byte | 2; - return MathHelper::unsignedRightShift32($temp * ($temp ^ 1), 8); - } - - public static function humanSize($size, $unit = "") - { - if ((!$unit && $size >= 1 << 30) || $unit == "GB") - return number_format($size / (1 << 30), 2) . "GB"; - if ((!$unit && $size >= 1 << 20) || $unit == "MB") - return number_format($size / (1 << 20), 2) . "MB"; - if ((!$unit && $size >= 1 << 10) || $unit == "KB") - return number_format($size / (1 << 10), 2) . "KB"; - return number_format($size) . " bytes"; - } - - -} \ No newline at end of file diff --git a/tests/PhpZip/ZipTest.php b/tests/PhpZip/ZipTest.php new file mode 100644 index 0000000..384a754 --- /dev/null +++ b/tests/PhpZip/ZipTest.php @@ -0,0 +1,1049 @@ +outputFilename = sys_get_temp_dir() . '/' . uniqid() . '.zip'; + } + + /** + * After test + */ + protected function tearDown() + { + parent::tearDown(); + + if ($this->outputFilename !== null && file_exists($this->outputFilename)) { + unlink($this->outputFilename); + } + } + + /** + * Create empty archive + * + * @see ZipOutputFile::create() + */ + public function testCreateEmptyArchive() + { + $zipFile = ZipOutputFile::create(); + $zipFile->saveAsFile($this->outputFilename); + $zipFile->close(); + + $zipFile = ZipFile::openFromFile($this->outputFilename); + self::assertEquals(count($zipFile), 0); + $zipFile->close(); + + self::assertCorrectEmptyZip($this->outputFilename); + } + + /** + * Create archive and add files. + * + * @see ZipOutputFile::addFromString() + * @see ZipOutputFile::addFromFile() + * @see ZipOutputFile::addFromStream() + * @see ZipFile::getEntryContent() + */ + public function testCreateArchiveAndAddFiles() + { + $outputFromString = file_get_contents(__FILE__); + $outputFromFile = file_get_contents(dirname(dirname(__DIR__)) . DIRECTORY_SEPARATOR . 'bootstrap.xml'); + $outputFromStream = file_get_contents(dirname(dirname(__DIR__)) . DIRECTORY_SEPARATOR . 'composer.json'); + + $filenameFromString = basename(__FILE__); + $filenameFromFile = 'data/test file.txt'; + $filenameFromStream = 'data/ডিরেক্টরি/αρχείο.json'; + $emptyDirName = 'empty dir/пустой каталог/空目錄/ไดเรกทอรีที่ว่างเปล่า/'; + + $tempFile = tempnam(sys_get_temp_dir(), 'txt'); + file_put_contents($tempFile, $outputFromFile); + + $tempStream = tmpfile(); + fwrite($tempStream, $outputFromStream); + + $outputZipFile = ZipOutputFile::create(); + $outputZipFile->addFromString($filenameFromString, $outputFromString); + $outputZipFile->addFromFile($tempFile, $filenameFromFile); + $outputZipFile->addFromStream($tempStream, $filenameFromStream); + $outputZipFile->addEmptyDir($emptyDirName); + $outputZipFile->saveAsFile($this->outputFilename); + $outputZipFile->close(); + unlink($tempFile); + + self::assertCorrectZipArchive($this->outputFilename); + + $zipFile = ZipFile::openFromFile($this->outputFilename); + self::assertEquals(count($zipFile), 4); + self::assertEquals($zipFile->getEntryContent($filenameFromString), $outputFromString); + self::assertEquals($zipFile->getEntryContent($filenameFromFile), $outputFromFile); + self::assertEquals($zipFile->getEntryContent($filenameFromStream), $outputFromStream); + self::assertTrue($zipFile->hasEntry($emptyDirName)); + self::assertTrue($zipFile->isDirectory($emptyDirName)); + + $listFiles = $zipFile->getListFiles(); + self::assertEquals($listFiles[0], $filenameFromString); + self::assertEquals($listFiles[1], $filenameFromFile); + self::assertEquals($listFiles[2], $filenameFromStream); + self::assertEquals($listFiles[3], $emptyDirName); + + $zipFile->close(); + } + + /** + * Create archive and add directory recursively. + */ + public function testAddDirRecursively() + { + $inputDir = dirname(dirname(__DIR__)) . DIRECTORY_SEPARATOR . "src"; + + $outputZipFile = new ZipOutputFile(); + $outputZipFile->addDir($inputDir); + $outputZipFile->saveAsFile($this->outputFilename); + $outputZipFile->close(); + + self::assertCorrectZipArchive($this->outputFilename); + } + + /** + * Create archive and add directory not recursively. + */ + public function testAddDirNotRecursively() + { + $inputDir = dirname(dirname(__DIR__)); + $recursive = false; + + $outputZipFile = new ZipOutputFile(); + $outputZipFile->addDir($inputDir, $recursive); + $outputZipFile->saveAsFile($this->outputFilename); + $outputZipFile->close(); + + self::assertCorrectZipArchive($this->outputFilename); + } + + /** + * Create archive and add directory and put files to path. + */ + public function testAddDirAndMoveToPath() + { + $inputDir = dirname(dirname(__DIR__)) . DIRECTORY_SEPARATOR . "src"; + + $recursive = true; + + $outputZipFile = new ZipOutputFile(); + $moveToPath = 'Library/src'; + $outputZipFile->addDir($inputDir, $recursive, $moveToPath); + $outputZipFile->saveAsFile($this->outputFilename); + $outputZipFile->close(); + + self::assertCorrectZipArchive($this->outputFilename); + } + + /** + * Create archive and add directory with ignore files list. + */ + public function testAddDirAndIgnoreFiles() + { + $inputDir = dirname(dirname(__DIR__)) . DIRECTORY_SEPARATOR; + + $recursive = false; + + $outputZipFile = new ZipOutputFile(); + $ignoreFiles = ['tests/', '.git/', 'composer.lock', 'vendor/', ".idea/"]; + $moveToPath = 'PhpZip Library'; + $outputZipFile->addDir($inputDir, $recursive, $moveToPath, $ignoreFiles); + $outputZipFile->saveAsFile($this->outputFilename); + $outputZipFile->close(); + + self::assertCorrectZipArchive($this->outputFilename); + } + + /** + * Create archive and add directory recursively with ignore files list. + */ + public function testAddDirAndIgnoreFilesRecursively() + { + $inputDir = dirname(dirname(__DIR__)) . DIRECTORY_SEPARATOR; + + $recursive = true; + + $outputZipFile = new ZipOutputFile(); + $ignoreFiles = ['tests/', '.git/', 'composer.lock', 'vendor/', ".idea/copyright/"]; + $moveToPath = 'PhpZip Library'; + $outputZipFile->addDir($inputDir, $recursive, $moveToPath, $ignoreFiles); + $outputZipFile->saveAsFile($this->outputFilename); + $outputZipFile->close(); + + self::assertCorrectZipArchive($this->outputFilename); + } + + /** + * Create archive and add files from glob pattern + */ + public function testAddFilesFromGlob() + { + $inputDir = dirname(dirname(__DIR__)) . DIRECTORY_SEPARATOR; + $moveToPath = null; + $recursive = false; + + $outputZipFile = new ZipOutputFile(); + $outputZipFile->addFilesFromGlob($inputDir, '**.{php,xml}', $moveToPath, $recursive); + $outputZipFile->saveAsFile($this->outputFilename); + $outputZipFile->close(); + + self::assertCorrectZipArchive($this->outputFilename); + } + + /** + * Create archive and add recursively files from glob pattern + */ + public function testAddFilesFromGlobRecursive() + { + $inputDir = dirname(dirname(__DIR__)) . DIRECTORY_SEPARATOR; + $moveToPath = "PhpZip Library"; + $recursive = true; + + $outputZipFile = new ZipOutputFile(); + $outputZipFile->addFilesFromGlob($inputDir, '**.{php,xml}', $recursive, $moveToPath); + $outputZipFile->saveAsFile($this->outputFilename); + $outputZipFile->close(); + + self::assertCorrectZipArchive($this->outputFilename); + } + + /** + * Create archive and add files from regex pattern + */ + public function testAddFilesFromRegex() + { + $inputDir = dirname(dirname(__DIR__)) . DIRECTORY_SEPARATOR; + $moveToPath = "Test"; + $recursive = false; + + $outputZipFile = new ZipOutputFile(); + $outputZipFile->addFilesFromRegex($inputDir, '~\.(xml|php)$~i', $recursive, $moveToPath); + $outputZipFile->saveAsFile($this->outputFilename); + $outputZipFile->close(); + + self::assertCorrectZipArchive($this->outputFilename); + } + + /** + * Create archive and add files recursively from regex pattern + */ + public function testAddFilesFromRegexRecursive() + { + $inputDir = dirname(dirname(__DIR__)) . DIRECTORY_SEPARATOR; + $moveToPath = "Test"; + $recursive = true; + + $outputZipFile = new ZipOutputFile(); + $outputZipFile->addFilesFromRegex($inputDir, '~\.(xml|php)$~i', $recursive, $moveToPath); + $outputZipFile->saveAsFile($this->outputFilename); + $outputZipFile->close(); + + self::assertCorrectZipArchive($this->outputFilename); + } + + /** + * Rename zip entry name. + */ + public function testRename() + { + $oldName = basename(__FILE__); + $newName = 'tests/' . $oldName; + + $outputZipFile = new ZipOutputFile(); + $outputZipFile->addDir(__DIR__); + $outputZipFile->saveAsFile($this->outputFilename); + $outputZipFile->close(); + + self::assertCorrectZipArchive($this->outputFilename); + + $zipFile = ZipFile::openFromFile($this->outputFilename); + $outputZipFile = new ZipOutputFile($zipFile); + $outputZipFile->rename($oldName, $newName); + $outputZipFile->saveAsFile($this->outputFilename); + $zipFile->close(); + + self::assertCorrectZipArchive($this->outputFilename); + + $zipFile = ZipFile::openFromFile($this->outputFilename); + self::assertFalse($zipFile->hasEntry($oldName)); + self::assertTrue($zipFile->hasEntry($newName)); + $zipFile->close(); + } + + /** + * Delete entry from name. + */ + public function testDeleteFromName() + { + $inputDir = dirname(dirname(__DIR__)) . DIRECTORY_SEPARATOR; + $deleteEntryName = 'composer.json'; + + $outputZipFile = new ZipOutputFile(); + $outputZipFile->addDir($inputDir, false); + $outputZipFile->saveAsFile($this->outputFilename); + $outputZipFile->close(); + + self::assertCorrectZipArchive($this->outputFilename); + + $zipFile = ZipFile::openFromFile($this->outputFilename); + $outputZipFile = new ZipOutputFile($zipFile); + $outputZipFile->deleteFromName($deleteEntryName); + $outputZipFile->saveAsFile($this->outputFilename); + $zipFile->close(); + + self::assertCorrectZipArchive($this->outputFilename); + + $zipFile = ZipFile::openFromFile($this->outputFilename); + self::assertFalse($zipFile->hasEntry($deleteEntryName)); + $zipFile->close(); + } + + /** + * Delete zip entries from glob pattern + */ + public function testDeleteFromGlob() + { + $inputDir = dirname(dirname(__DIR__)); + + $outputZipFile = new ZipOutputFile(); + $outputZipFile->addFilesFromGlob($inputDir, '**.{php,xml,json}', true); + $outputZipFile->saveAsFile($this->outputFilename); + $outputZipFile->close(); + + self::assertCorrectZipArchive($this->outputFilename); + + $zipFile = ZipFile::openFromFile($this->outputFilename); + $outputZipFile = new ZipOutputFile($zipFile); + $outputZipFile->deleteFromGlob('**.{xml,json}'); + $outputZipFile->saveAsFile($this->outputFilename); + $zipFile->close(); + + self::assertCorrectZipArchive($this->outputFilename); + + $zipFile = ZipFile::openFromFile($this->outputFilename); + self::assertFalse($zipFile->hasEntry('composer.json')); + self::assertFalse($zipFile->hasEntry('bootstrap.xml')); + $zipFile->close(); + } + + /** + * Delete entries from regex pattern + */ + public function testDeleteFromRegex() + { + $inputDir = dirname(dirname(__DIR__)); + + $outputZipFile = new ZipOutputFile(); + $outputZipFile->addFilesFromRegex($inputDir, '~\.(xml|php|json)$~i', true, 'Path'); + $outputZipFile->saveAsFile($this->outputFilename); + $outputZipFile->close(); + + self::assertCorrectZipArchive($this->outputFilename); + + $zipFile = ZipFile::openFromFile($this->outputFilename); + $outputZipFile = new ZipOutputFile($zipFile); + $outputZipFile->deleteFromRegex('~\.(json)$~i'); + $outputZipFile->saveAsFile($this->outputFilename); + $zipFile->close(); + + self::assertCorrectZipArchive($this->outputFilename); + + $zipFile = ZipFile::openFromFile($this->outputFilename); + self::assertFalse($zipFile->hasEntry('Path/composer.json')); + self::assertTrue($zipFile->hasEntry('Path/bootstrap.xml')); + $zipFile->close(); + } + + /** + * Delete all entries + */ + public function testDeleteAll() + { + $outputZipFile = new ZipOutputFile(); + $outputZipFile->addDir(__DIR__); + $outputZipFile->saveAsFile($this->outputFilename); + $outputZipFile->close(); + + self::assertCorrectZipArchive($this->outputFilename); + + $zipFile = ZipFile::openFromFile($this->outputFilename); + self::assertTrue($zipFile->count() > 0); + + $outputZipFile = new ZipOutputFile($zipFile); + $outputZipFile->deleteAll(); + $outputZipFile->saveAsFile($this->outputFilename); + $zipFile->close(); + + self::assertCorrectEmptyZip($this->outputFilename); + + $zipFile = ZipFile::openFromFile($this->outputFilename); + self::assertEquals($zipFile->count(), 0); + $zipFile->close(); + } + + /** + * Test zip archive comment. + */ + public function testArchiveComment() + { + $comment = "This zip file comment" . PHP_EOL + . "Αυτό το σχόλιο αρχείο zip" . PHP_EOL + . "Это комментарий zip архива" . PHP_EOL + . "這個ZIP文件註釋" . PHP_EOL + . "ეს zip ფაილის კომენტარი" . PHP_EOL + . "このzipファイルにコメント" . PHP_EOL + . "ความคิดเห็นนี้ไฟล์ซิป"; + + $outputZipFile = new ZipOutputFile(); + $outputZipFile->setComment($comment); + $outputZipFile->addFromFile(__FILE__); + $outputZipFile->saveAsFile($this->outputFilename); + $outputZipFile->close(); + + self::assertCorrectZipArchive($this->outputFilename); + + $zipFile = ZipFile::openFromFile($this->outputFilename); + self::assertEquals($zipFile->getComment(), $comment); + // remove comment + $outputZipFile = ZipOutputFile::openFromZipFile($zipFile); + $outputZipFile->setComment(null); + $outputZipFile->saveAsFile($this->outputFilename); + $outputZipFile->close(); + $zipFile->close(); + + self::assertCorrectZipArchive($this->outputFilename); + + // check empty comment + $zipFile = ZipFile::openFromFile($this->outputFilename); + self::assertEquals($zipFile->getComment(), ""); + $zipFile->close(); + } + + /** + * Test very long archive comment. + * + * @expectedException \PhpZip\Exception\IllegalArgumentException + */ + public function testVeryLongArchiveComment() + { + $comment = "Very long comment" . PHP_EOL . + "Очень длинный комментарий" . PHP_EOL; + $comment = str_repeat($comment, ceil(0xffff / strlen($comment)) + strlen($comment) + 1); + + $outputZipFile = new ZipOutputFile(); + $outputZipFile->setComment($comment); + } + + /** + * Test zip entry comment. + */ + public function testEntryComment() + { + $entries = [ + '文件1.txt' => [ + 'data' => CryptoUtil::randomBytes(255), + 'comment' => "這是註釋的條目。", + ], + 'file2.txt' => [ + 'data' => CryptoUtil::randomBytes(255), + 'comment' => null + ], + 'file3.txt' => [ + 'data' => CryptoUtil::randomBytes(255), + 'comment' => CryptoUtil::randomBytes(255), + ], + 'file4.txt' => [ + 'data' => CryptoUtil::randomBytes(255), + 'comment' => "Комментарий файла" + ], + 'file5.txt' => [ + 'data' => CryptoUtil::randomBytes(255), + 'comment' => "ไฟล์แสดงความคิดเห็น" + ], + 'file6 emoji 🙍🏼.txt' => [ + 'data' => CryptoUtil::randomBytes(255), + 'comment' => "Emoji comment file - 😀 ⛈ ❤️ 🤴🏽" + ], + ]; + + $outputZipFile = new ZipOutputFile(); + foreach ($entries as $entryName => $item) { + $outputZipFile->addFromString($entryName, $item['data']); + $outputZipFile->setEntryComment($entryName, $item['comment']); + } + $outputZipFile->saveAsFile($this->outputFilename); + $outputZipFile->close(); + + self::assertCorrectZipArchive($this->outputFilename); + + $zipFile = ZipFile::openFromFile($this->outputFilename); + foreach ($zipFile->getListFiles() as $entryName) { + $entriesItem = $entries[$entryName]; + self::assertNotEmpty($entriesItem); + self::assertEquals($zipFile->getEntryContent($entryName), $entriesItem['data']); + self::assertEquals($zipFile->getEntryComment($entryName), (string)$entriesItem['comment']); + } + $zipFile->close(); + } + + /** + * Test zip entry very long comment. + * + * @expectedException \PhpZip\Exception\ZipException + */ + public function testVeryLongEntryComment() + { + $comment = "Very long comment" . PHP_EOL . + "Очень длинный комментарий" . PHP_EOL; + $comment = str_repeat($comment, ceil(0xffff / strlen($comment)) + strlen($comment) + 1); + + $outputZipFile = new ZipOutputFile(); + $outputZipFile->addFromFile(__FILE__, 'test'); + $outputZipFile->setEntryComment('test', $comment); + } + + /** + * Test set illegal compression method. + * + * @expectedException \PhpZip\Exception\IllegalArgumentException + */ + public function testIllegalCompressionMethod() + { + $outputZipFile = new ZipOutputFile(); + $outputZipFile->addFromFile(__FILE__, null, ZipEntry::WINZIP_AES); + } + + /** + * Test all available support compression methods. + */ + public function testCompressionMethod() + { + $entries = [ + '1' => [ + 'data' => CryptoUtil::randomBytes(255), + 'method' => ZipEntry::METHOD_STORED, + ], + '2' => [ + 'data' => CryptoUtil::randomBytes(255), + 'method' => ZipEntry::METHOD_DEFLATED, + ], + ]; + if (extension_loaded("bz2")) { + $entries['3'] = [ + 'data' => CryptoUtil::randomBytes(255), + 'method' => ZipEntry::METHOD_BZIP2, + ]; + } + + $outputZipFile = new ZipOutputFile(); + foreach ($entries as $entryName => $item) { + $outputZipFile->addFromString($entryName, $item['data'], $item['method']); + } + $outputZipFile->saveAsFile($this->outputFilename); + $outputZipFile->close(); + + self::assertCorrectZipArchive($this->outputFilename); + + $zipFile = ZipFile::openFromFile($this->outputFilename); + $outputZipFile = ZipOutputFile::openFromZipFile($zipFile); + $outputZipFile->setLevel(ZipOutputFile::LEVEL_BEST_COMPRESSION); + foreach ($zipFile->getRawEntries() as $entry) { + self::assertEquals($zipFile->getEntryContent($entry->getName()), $entries[$entry->getName()]['data']); + self::assertEquals($entry->getMethod(), $entries[$entry->getName()]['method']); + + switch ($entry->getMethod()) { + case ZipEntry::METHOD_STORED: + $entries[$entry->getName()]['method'] = ZipEntry::METHOD_DEFLATED; + $outputZipFile->setCompressionMethod($entry->getName(), ZipEntry::METHOD_DEFLATED); + break; + + case ZipEntry::METHOD_DEFLATED: + $entries[$entry->getName()]['method'] = ZipEntry::METHOD_STORED; + $outputZipFile->setCompressionMethod($entry->getName(), ZipEntry::METHOD_STORED); + break; + } + } + $outputZipFile->saveAsFile($this->outputFilename); + $outputZipFile->close(); + $zipFile->close(); + + self::assertCorrectZipArchive($this->outputFilename); + + $zipFile = ZipFile::openFromFile($this->outputFilename); + foreach ($zipFile->getRawEntries() as $entry) { + $actualEntry = $entries[$entry->getName()]; + + self::assertEquals($zipFile->getEntryContent($entry->getName()), $actualEntry['data']); + self::assertEquals($entry->getMethod(), $actualEntry['method']); + } + $zipFile->close(); + } + + /** + * Test extract all files. + */ + public function testExtract() + { + $entries = [ + 'test1.txt' => CryptoUtil::randomBytes(255), + 'test2.txt' => CryptoUtil::randomBytes(255), + 'test/test 2/test3.txt' => CryptoUtil::randomBytes(255), + 'test empty/dir' => null, + ]; + + $outputFolderInput = sys_get_temp_dir() . '/zipExtract' . uniqid(); + if (!is_dir($outputFolderInput)) { + mkdir($outputFolderInput, 0755, true); + } + $outputFolderOutput = sys_get_temp_dir() . '/zipExtract' . uniqid(); + if (!is_dir($outputFolderOutput)) { + mkdir($outputFolderOutput, 0755, true); + } + + $outputZipFile = new ZipOutputFile(); + foreach ($entries as $entryName => $value) { + if ($value === null) { + $outputZipFile->addEmptyDir($entryName); + } else { + $outputZipFile->addFromString($entryName, $value); + } + } + $outputZipFile->saveAsFile($this->outputFilename); + $outputZipFile->close(); + + $zipFile = ZipFile::openFromFile($this->outputFilename); + $zipFile->extractTo($outputFolderInput); + + $outputZipFile = new ZipOutputFile($zipFile); + $outputZipFile->extractTo($outputFolderOutput); + foreach ($entries as $entryName => $value) { + $fullInputFilename = $outputFolderInput . DIRECTORY_SEPARATOR . $entryName; + $fullOutputFilename = $outputFolderOutput . DIRECTORY_SEPARATOR . $entryName; + if ($value === null) { + self::assertTrue(is_dir($fullInputFilename)); + self::assertTrue(is_dir($fullOutputFilename)); + + self::assertTrue(FilesUtil::isEmptyDir($fullInputFilename)); + self::assertTrue(FilesUtil::isEmptyDir($fullOutputFilename)); + } else { + self::assertTrue(is_file($fullInputFilename)); + self::assertTrue(is_file($fullOutputFilename)); + + $contentInput = file_get_contents($fullInputFilename); + $contentOutput = file_get_contents($fullOutputFilename); + self::assertEquals($contentInput, $value); + self::assertEquals($contentOutput, $value); + self::assertEquals($contentInput, $contentOutput); + } + } + $outputZipFile->close(); + $zipFile->close(); + + FilesUtil::removeDir($outputFolderInput); + FilesUtil::removeDir($outputFolderOutput); + } + + /** + * Test extract some files + */ + public function testExtractSomeFiles() + { + $entries = [ + 'test1.txt' => CryptoUtil::randomBytes(255), + 'test2.txt' => CryptoUtil::randomBytes(255), + 'test3.txt' => CryptoUtil::randomBytes(255), + 'test4.txt' => CryptoUtil::randomBytes(255), + 'test5.txt' => CryptoUtil::randomBytes(255), + 'test/test/test.txt' => CryptoUtil::randomBytes(255), + 'test/test/test 2.txt' => CryptoUtil::randomBytes(255), + 'test empty/dir/' => null, + 'test empty/dir2/' => null, + ]; + + $extractEntries = ['test1.txt', 'test3.txt', 'test5.txt', 'test/test/test 2.txt', 'test empty/dir2/']; + + $outputFolderInput = sys_get_temp_dir() . '/zipExtract' . uniqid(); + if (!is_dir($outputFolderInput)) { + mkdir($outputFolderInput, 0755, true); + } + $outputFolderOutput = sys_get_temp_dir() . '/zipExtract' . uniqid(); + if (!is_dir($outputFolderOutput)) { + mkdir($outputFolderOutput, 0755, true); + } + + $outputZipFile = new ZipOutputFile(); + foreach ($entries as $entryName => $value) { + if ($value === null) { + $outputZipFile->addEmptyDir($entryName); + } else { + $outputZipFile->addFromString($entryName, $value); + } + } + $outputZipFile->saveAsFile($this->outputFilename); + $outputZipFile->close(); + + $zipFile = ZipFile::openFromFile($this->outputFilename); + $zipFile->extractTo($outputFolderInput, $extractEntries); + + $outputZipFile = new ZipOutputFile($zipFile); + $outputZipFile->extractTo($outputFolderOutput, $extractEntries); + foreach ($entries as $entryName => $value) { + $fullInputFilename = $outputFolderInput . DIRECTORY_SEPARATOR . $entryName; + $fullOutputFilename = $outputFolderOutput . DIRECTORY_SEPARATOR . $entryName; + if (in_array($entryName, $extractEntries)) { + if ($value === null) { + self::assertTrue(is_dir($fullInputFilename)); + self::assertTrue(is_dir($fullOutputFilename)); + + self::assertTrue(FilesUtil::isEmptyDir($fullInputFilename)); + self::assertTrue(FilesUtil::isEmptyDir($fullOutputFilename)); + } else { + self::assertTrue(is_file($fullInputFilename)); + self::assertTrue(is_file($fullOutputFilename)); + + $contentInput = file_get_contents($fullInputFilename); + $contentOutput = file_get_contents($fullOutputFilename); + self::assertEquals($contentInput, $value); + self::assertEquals($contentOutput, $value); + self::assertEquals($contentInput, $contentOutput); + } + } else { + if ($value === null) { + self::assertFalse(is_dir($fullInputFilename)); + self::assertFalse(is_dir($fullOutputFilename)); + } else { + self::assertFalse(is_file($fullInputFilename)); + self::assertFalse(is_file($fullOutputFilename)); + } + } + } + $outputZipFile->close(); + $zipFile->close(); + + FilesUtil::removeDir($outputFolderInput); + FilesUtil::removeDir($outputFolderOutput); + } + + /** + * Test archive password. + */ + public function testSetPassword() + { + $password = CryptoUtil::randomBytes(100); + $badPassword = "sdgt43r23wefe"; + + $outputZip = ZipOutputFile::create(); + $outputZip->addDir(__DIR__); + $outputZip->setPassword($password, ZipEntry::ENCRYPTION_METHOD_TRADITIONAL); + $outputZip->saveAsFile($this->outputFilename); + $outputZip->close(); + + $zipFile = ZipFile::openFromFile($this->outputFilename); + + // set bad password Traditional Encryption + $zipFile->setPassword($badPassword); + foreach ($zipFile->getListFiles() as $entryName) { + try { + $zipFile->getEntryContent($entryName); + self::fail("Expected Exception has not been raised."); + } catch (ZipAuthenticationException $ae) { + self::assertNotNull($ae); + } + } + + // set correct password + $zipFile->setPassword($password); + foreach ($zipFile->getAllInfo() as $info) { + self::assertTrue($info->isEncrypted()); + self::assertContains('ZipCrypto', $info->getMethod()); + $decryptContent = $zipFile->getEntryContent($info->getPath()); + self::assertNotEmpty($decryptContent); + self::assertContains('setPassword($password, ZipEntry::ENCRYPTION_METHOD_WINZIP_AES); + $outputZip->saveAsFile($this->outputFilename); + $outputZip->close(); + $zipFile->close(); + + // check from WinZip AES encryption + $zipFile = ZipFile::openFromFile($this->outputFilename); + + // set bad password WinZip AES + $zipFile->setPassword($badPassword); + foreach ($zipFile->getListFiles() as $entryName) { + try { + $zipFile->getEntryContent($entryName); + self::fail("Expected Exception has not been raised."); + } catch (ZipAuthenticationException $ae) { + self::assertNotNull($ae); + } + } + + // set correct password WinZip AES + $zipFile->setPassword($password); + foreach ($zipFile->getAllInfo() as $info) { + self::assertTrue($info->isEncrypted()); + self::assertContains('WinZip', $info->getMethod()); + $decryptContent = $zipFile->getEntryContent($info->getPath()); + self::assertNotEmpty($decryptContent); + self::assertContains('removePasswordAllEntries(); + $outputZip->saveAsFile($this->outputFilename); + $outputZip->close(); + $zipFile->close(); + + self::assertCorrectZipArchive($this->outputFilename); + + // check remove password + $zipFile = ZipFile::openFromFile($this->outputFilename); + foreach ($zipFile->getAllInfo() as $info) { + self::assertFalse($info->isEncrypted()); + } + $zipFile->close(); + } + + /** + * Test set password to some entries. + */ + public function testSetPasswordToSomeEntries() + { + $entries = [ + 'Traditional PKWARE Encryption Test.dat' => [ + 'data' => CryptoUtil::randomBytes(255), + 'password' => CryptoUtil::randomBytes(255), + 'encryption_method' => ZipEntry::ENCRYPTION_METHOD_TRADITIONAL, + 'compression_method' => ZipEntry::METHOD_DEFLATED, + ], + 'WinZip AES Encryption Test.dat' => [ + 'data' => CryptoUtil::randomBytes(255), + 'password' => CryptoUtil::randomBytes(255), + 'encryption_method' => ZipEntry::ENCRYPTION_METHOD_WINZIP_AES, + 'compression_method' => ZipEntry::METHOD_BZIP2, + ], + 'Not password.dat' => [ + 'data' => CryptoUtil::randomBytes(255), + 'password' => null, + 'encryption_method' => ZipEntry::ENCRYPTION_METHOD_TRADITIONAL, + 'compression_method' => ZipEntry::METHOD_STORED, + ], + ]; + + $outputZip = ZipOutputFile::create(); + foreach ($entries as $entryName => $item) { + $outputZip->addFromString($entryName, $item['data'], $item['compression_method']); + if ($item['password'] !== null) { + $outputZip->setEntryPassword($entryName, $item['password'], $item['encryption_method']); + } + } + $outputZip->saveAsFile($this->outputFilename); + $outputZip->close(); + + $outputDir = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'zipextract' . uniqid(); + if (!is_dir($outputDir)) { + self::assertTrue(mkdir($outputDir, 0755, true)); + } + + $zipFile = ZipFile::openFromFile($this->outputFilename); + foreach ($entries as $entryName => $item) { + if ($item['password'] !== null) { + $zipFile->setEntryPassword($entryName, $item['password']); + } + } + $zipFile->extractTo($outputDir); + $zipFile->close(); + + self::assertFalse(FilesUtil::isEmptyDir($outputDir)); + + foreach ($entries as $entryName => $item) { + self::assertEquals(file_get_contents($outputDir . DIRECTORY_SEPARATOR . $entryName), $item['data']); + } + + FilesUtil::removeDir($outputDir); + } + + /** + * Test `ZipFile` implemented \ArrayAccess, \Countable and |iterator. + */ + public function testZipFileArrayAccessAndCountableAndIterator() + { + $files = []; + $numFiles = mt_rand(20, 100); + for ($i = 0; $i < $numFiles; $i++) { + $files['file' . $i . '.txt'] = CryptoUtil::randomBytes(255); + } + + $methods = [ZipEntry::METHOD_STORED, ZipEntry::METHOD_DEFLATED]; + if (extension_loaded("bz2")) { + $methods[] = ZipEntry::METHOD_BZIP2; + } + + $zipOutputFile = ZipOutputFile::create(); + $zipOutputFile->setLevel(ZipOutputFile::LEVEL_BEST_SPEED); + foreach ($files as $entryName => $content) { + $zipOutputFile->addFromString($entryName, $content, $methods[array_rand($methods)]); + } + $zipOutputFile->saveAsFile($this->outputFilename); + $zipOutputFile->close(); + + self::assertCorrectZipArchive($this->outputFilename); + + $zipFile = ZipFile::openFromFile($this->outputFilename); + + // Test \Countable + self::assertEquals($zipFile->count(), $numFiles); + self::assertEquals(count($zipFile), $numFiles); + + // Test \ArrayAccess + reset($files); + foreach ($zipFile as $entryName => $content) { + self::assertEquals($entryName, key($files)); + self::assertEquals($content, current($files)); + next($files); + } + + // Test \Iterator + reset($files); + $iterator = new \ArrayIterator($zipFile); + $iterator->rewind(); + while ($iterator->valid()) { + $key = $iterator->key(); + $value = $iterator->current(); + + self::assertEquals($key, key($files)); + self::assertEquals($value, current($files)); + + next($files); + $iterator->next(); + } + $zipFile->close(); + } + + /** + * Test `ZipOutputFile` implemented \ArrayAccess, \Countable and |iterator. + */ + public function testZipOutputFileArrayAccessAndCountableAndIterator() + { + $files = []; + $numFiles = mt_rand(20, 100); + for ($i = 0; $i < $numFiles; $i++) { + $files['file' . $i . '.txt'] = CryptoUtil::randomBytes(255); + } + + $methods = [ZipEntry::METHOD_STORED, ZipEntry::METHOD_DEFLATED]; + if (extension_loaded("bz2")) { + $methods[] = ZipEntry::METHOD_BZIP2; + } + + $zipOutputFile = ZipOutputFile::create(); + $zipOutputFile->setLevel(ZipOutputFile::LEVEL_BEST_SPEED); + foreach ($files as $entryName => $content) { + $zipOutputFile->addFromString($entryName, $content, $methods[array_rand($methods)]); + } + $zipOutputFile->saveAsFile($this->outputFilename); + $zipOutputFile->close(); + + self::assertCorrectZipArchive($this->outputFilename); + + $zipFile = ZipFile::openFromFile($this->outputFilename); + $zipOutputFile = ZipOutputFile::openFromZipFile($zipFile); + + // Test \Countable + self::assertEquals($zipOutputFile->count(), $numFiles); + self::assertEquals(count($zipOutputFile), $numFiles); + + // Test \ArrayAccess + reset($files); + foreach ($zipOutputFile as $entryName => $content) { + self::assertEquals($entryName, key($files)); + self::assertEquals($content, current($files)); + next($files); + } + + // Test \Iterator + reset($files); + $iterator = new \ArrayIterator($zipOutputFile); + $iterator->rewind(); + while ($iterator->valid()) { + $key = $iterator->key(); + $value = $iterator->current(); + + self::assertEquals($key, key($files)); + self::assertEquals($value, current($files)); + + next($files); + $iterator->next(); + } + + // Test set and unset + $zipOutputFile['new entry name'] = 'content'; + unset($zipOutputFile['file0.txt'], $zipOutputFile['file1.txt'], $zipOutputFile['file2.txt']); + $zipOutputFile->saveAsFile($this->outputFilename); + $zipOutputFile->close(); + $zipFile->close(); + + $zipFile = ZipFile::openFromFile($this->outputFilename); + self::assertEquals($numFiles + 1 - 3, sizeof($zipFile)); + self::assertTrue(isset($zipFile['new entry name'])); + self::assertEquals($zipFile['new entry name'], 'content'); + self::assertFalse(isset($zipFile['file0.txt'])); + self::assertFalse(isset($zipFile['file1.txt'])); + self::assertFalse(isset($zipFile['file2.txt'])); + self::assertTrue(isset($zipFile['file3.txt'])); + $zipFile->close(); + } + + /** + * Test support ZIP64 ext (slow test - normal). + */ + public function testCreateAndOpenZip64Ext() + { + $countFiles = 0xffff + 1; + + $outputZipFile = ZipOutputFile::create(); + for ($i = 0; $i < $countFiles; $i++) { + $outputZipFile->addFromString($i . '.txt', $i, ZipEntry::METHOD_STORED); + } + $outputZipFile->saveAsFile($this->outputFilename); + $outputZipFile->close(); + + self::assertCorrectZipArchive($this->outputFilename); + + $zipFile = ZipFile::openFromFile($this->outputFilename); + self::assertEquals($zipFile->count(), $countFiles); + foreach ($zipFile->getListFiles() as $entry) { + $zipFile->getEntryContent($entry); + } + $zipFile->close(); + } + +} \ No newline at end of file diff --git a/tests/PhpZip/ZipTestCase.php b/tests/PhpZip/ZipTestCase.php new file mode 100644 index 0000000..a762512 --- /dev/null +++ b/tests/PhpZip/ZipTestCase.php @@ -0,0 +1,45 @@ +create(); - $zip->addFile(__FILE__); - $zip->addFromString($listFilename, $listFileContent); - $zip->addEmptyDir($dirName); - $zip->setArchiveComment($archiveComment); - $zip->setCommentIndex(0, $commentIndex0); - $zip->saveAs($output); - $zip->close(); - - $this->assertTrue(file_exists($output)); - $this->assertCorrectZipArchive($output); - - $zip = new \Nelexa\Zip\ZipFile(); - $zip->open($output); - $listFiles = $zip->getListFiles(); - - $this->assertEquals(sizeof($listFiles), 3); - $filenameIndex0 = basename(__FILE__); - $this->assertEquals($listFiles[0], $filenameIndex0); - $this->assertEquals($listFiles[1], $listFilename); - $this->assertEquals($listFiles[2], $dirName); - - $this->assertEquals($zip->getFromIndex(0), $zip->getFromName(basename(__FILE__))); - $this->assertEquals($zip->getFromIndex(0), file_get_contents(__FILE__)); - $this->assertEquals($zip->getFromIndex(1), $zip->getFromName($listFilename)); - $this->assertEquals($zip->getFromIndex(1), $listFileContent); - - $this->assertEquals($zip->getArchiveComment(), $archiveComment); - $this->assertEquals($zip->getCommentIndex(0), $commentIndex0); - - if (!file_exists($extractOutputDir)) { - $this->assertTrue(mkdir($extractOutputDir, 0755, true)); - } - - $zip->extractTo($extractOutputDir); - - $this->assertTrue(file_exists($extractOutputDir . DIRECTORY_SEPARATOR . $filenameIndex0)); - $this->assertEquals(md5_file($extractOutputDir . DIRECTORY_SEPARATOR . $filenameIndex0), md5_file(__FILE__)); - - $this->assertTrue(file_exists($extractOutputDir . DIRECTORY_SEPARATOR . $listFilename)); - $this->assertEquals(file_get_contents($extractOutputDir . DIRECTORY_SEPARATOR . $listFilename), $listFileContent); - - $zip->close(); - - unlink($output); - - $files = new RecursiveIteratorIterator( - new RecursiveDirectoryIterator($extractOutputDir, RecursiveDirectoryIterator::SKIP_DOTS), - RecursiveIteratorIterator::CHILD_FIRST - ); - - foreach ($files as $fileInfo) { - $todo = ($fileInfo->isDir() ? 'rmdir' : 'unlink'); - $todo($fileInfo->getRealPath()); - } - - rmdir($extractOutputDir); - } - - /** - * - */ - public function testUpdate() - { - $file = __DIR__ . '/res/file.apk'; - $privateKey = __DIR__ . '/res/private.pem'; - $publicKey = __DIR__ . '/res/public.pem'; - $outputFile = sys_get_temp_dir() . '/test-update.apk'; - - $zip = new \Nelexa\Zip\ZipFile($file); - $zip->open($file); - - // signed apk file - $certList = array(); - $manifestMf = new Manifest(); - $manifestMf->appendLine("Manifest-Version: 1.0"); - $manifestMf->appendLine("Created-By: 1.0 (Android)"); - $manifestMf->appendLine(''); - for ($i = 0, $length = $zip->getCountFiles(); $i < $length; $i++) { - $name = $zip->getNameIndex($i); - if ($name[strlen($name) - 1] === '/') continue; // is path - $content = $zip->getFromIndex($i); - - $certManifest = $this->createSha1EncodeEntryManifest($name, $content); - $manifestMf->appendManifest($certManifest); - $certList[$name] = $certManifest; - } - $manifestMf = $manifestMf->getContent(); - - $certSf = new Manifest(); - $certSf->appendLine('Signature-Version: 1.0'); - $certSf->appendLine('Created-By: 1.0 (Android)'); - $certSf->appendLine('SHA1-Digest-Manifest: ' . base64_encode(sha1($manifestMf, 1))); - $certSf->appendLine(''); - foreach ($certList AS $filename => $content) { - $certManifest = $this->createSha1EncodeEntryManifest($filename, $content->getContent()); - $certSf->appendManifest($certManifest); - } - $certSf = $certSf->getContent(); - unset($certList); - - $zip->addFromString('META-INF/MANIFEST.MF', $manifestMf); - $zip->addFromString('META-INF/CERT.SF', $certSf); - - if (`which openssl`) { - $openssl_cmd = 'printf ' . escapeshellarg($certSf) . ' | openssl smime -md sha1 -sign -inkey ' . escapeshellarg($privateKey) . ' -signer ' . $publicKey . ' -binary -outform DER -noattr'; - - ob_start(); - passthru($openssl_cmd, $error); - $rsaContent = ob_get_clean(); - $this->assertEquals($error, 0); - - $zip->addFromString('META-INF/CERT.RSA', $rsaContent); - } - - $zip->saveAs($outputFile); - $zip->close(); - - $this->assertCorrectZipArchive($outputFile); - - if (`which jarsigner`) { - ob_start(); - passthru('jarsigner -verify -verbose -certs ' . escapeshellarg($outputFile), $error); - $verifedResult = ob_get_clean(); - - $this->assertEquals($error, 0); - $this->assertContains('jar verified', $verifedResult); - } - - unlink($outputFile); - } - - /** - * @param $filename - */ - private function assertCorrectZipArchive($filename) - { - exec("zip -T " . escapeshellarg($filename), $output, $returnCode); - $this->assertEquals($returnCode, 0); - } - - /** - * @param string $filename - * @param string $content - * @return Manifest - */ - private function createSha1EncodeEntryManifest($filename, $content) - { - $manifest = new Manifest(); - $manifest->appendLine('Name: ' . $filename); - $manifest->appendLine('SHA1-Digest: ' . base64_encode(sha1($content, 1))); - return $manifest; - } -} - -class Manifest -{ - private $content; - - /** - * @return mixed - */ - public function getContent() - { - return trim($this->content) . "\r\n\r\n"; - } - - /** - * Process a long manifest line and add continuation if required - * @param $line string - * @return Manifest - */ - public function appendLine($line) - { - $begin = 0; - $sb = ''; - $lineLength = mb_strlen($line, "UTF-8"); - for ($end = 70; $lineLength - $begin > 70; $end += 69) { - $sb .= mb_substr($line, $begin, $end - $begin, "UTF-8") . "\r\n "; - $begin = $end; - } - $this->content .= $sb . mb_substr($line, $begin, $lineLength, "UTF-8") . "\r\n"; - return $this; - } - - public function appendManifest(Manifest $manifest) - { - $this->content .= $manifest->getContent(); - return $this; - } - - public function clear() - { - $this->content = ''; - } - - /** - * @param string $manifestContent - * @return Manifest - */ - public static function createFromManifest($manifestContent) - { - $manifestContent = trim($manifestContent); - $lines = explode("\n", $manifestContent); - - // normalize manifest - $content = ''; - $trim = array("\r", "\n"); - foreach ($lines AS $line) { - - $line = str_replace($trim, '', $line); - if ($line[0] === ' ') { - $content = rtrim($content, "\n\r"); - $line = ltrim($line); - } - $content .= $line . "\r\n"; - } - - $manifset = new self; - $lines = explode("\n", $content); - foreach ($lines AS $line) { - $line = trim($line, "\n\r"); - $manifset->appendLine($line); - } - return $manifset; - } -} \ No newline at end of file diff --git a/tests/res/file.apk b/tests/res/file.apk deleted file mode 100644 index d58c64b..0000000 Binary files a/tests/res/file.apk and /dev/null differ diff --git a/tests/res/private.pem b/tests/res/private.pem deleted file mode 100644 index 0af9845..0000000 --- a/tests/res/private.pem +++ /dev/null @@ -1,28 +0,0 @@ ------BEGIN PRIVATE KEY----- -MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCwiURw5w8TAS0G -iWaBzJqbQyacFsIzKc+orHDzBdBdKlrx/aUlw3fBA310aP7343hc7vMm6koar/1n -8iEh2W4retDHzNf9j3IXETa5/D+3WJY4aVJkYcFr8v6OEnuSXIwKZduMeL4BpQwM -Xu/z6gkoTa9o+Dzhl46l71UX8umdPIx/4YWwX2oSm6EGklcGJyYdqMvwOXVXDE/J -qqPzC7ZQiu422cDDvqBYt32CVyjLKCo5YjWBb24jxjtl5M4y8xPOcHlVEG2WwPBU -sv2aw4Z9/Q0erZ35gMyzu+Y+2623B3tuAbfsswgBINq0bl2v+M17Kpv2tjhMKj1H -ds6CK/BVAgMBAAECggEAdTUt86f1IjENq+Fd5Z/qplsXL1sM5NtFvD+BXljl1nVg -nHpDQ6dbwxKGINv1LLAiIdGkLpovSTi/jlv8E3VA6C1KoN0oKnkqzpXnN+R6iUiP -tDR5N5yPxxQ2Xi13Td2UPPMTqVghDwZ90VjXB6LDIbcyVwc5pK3zT8hvPs9Qu8t1 -S2pCEKcowvTRSB1DMTZ3lrNjEEIMdV0H8Qik3lf7ognRGoDywu5pA1bc/Yg+XlmP -/ZmQinFeg3izNQzDdP6Ppo1i/QFeVXVuMs2ergMMHJRNUhBXKz8iNyVupqfroE8a -xRpD3eO+KvSNb0TJR5TXf64t62zEEpHaRsmgACEMAQKBgQDXo0jVUa67oqfJBFBU -3zfHIlgcrydRE4Av+LJ0hdEnFpMwVpUihJXUaJijawTzTKOgpVImhxfr/T1HMalm -MTXH5Tc7inJTiB9A1IffLPqgoOr2JRwQ2q8lgWkQPkq1ySd+q0vhkj1tuAe3qI1i -jiMo1Vb9zdVjcxmvPnZRKJgiIQKBgQDRlFm6PKc2Zx46BXeNPtXnHhSduUBJf2iO -n9/pKTANQuDlPwC3Q4edSKe44fZ/oj4KRAnzX254wXBMX+ktKX/kqXbwEanxcd/v -Lnvgv8QhsEKO3Ye09yasAfC2lYsSVSwHv+dYurb0nZ2JEPL1IP+V76RgTbdeMdic -Mt53jN/vtQKBgQC+D+mOO+Sq9X61qtuzMtvS5O6MucUJrQp7PdTs51WmAjvRiz7/ -oaT+BwMiZp2CZLaETbLOypvHIPn12kvZCt7ARcQc8rY58ey6E5l+mAJ/udXfBm5q -XJWrlRipfH4VJCtvdkP3mhIStvX2ZtXXXDiZMRDvu5CtizHESGW4uvL8gQKBgCI7 -ukBagfG3/E7776BJwETlO/bbeK3Iuvp5EOkUCj5QS04G8YX96Nv/Ly5a8pm8lae1 -n256ix/8cOx4yizPV42xRLVIHVtL/4khLajzigT6tpSBiRY9PLriAkDAwpu2/98w -MIjkzte8Gyx1cUorHrSOFWqJp0cim0BAauhaQYX1AoGAPvb5TG/A6LhYKgCWUMOH -lucrnV3Ns1BzaaMmOaokuYGtyTv2loVlrv+7QGdC9UBRXDz46DTE7CPHBwReNnWB -R7YW50VwB9YfD7dqRM24Y08F3B7RCNhqsAnpAtVgXf+/01o2nfJbzxTty/STiBNB -OjjxKHnAgIfhe7xAIiY2eow= ------END PRIVATE KEY----- diff --git a/tests/res/public.pem b/tests/res/public.pem deleted file mode 100644 index e07374f..0000000 --- a/tests/res/public.pem +++ /dev/null @@ -1,21 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIDeTCCAmGgAwIBAgIEDeVWNjANBgkqhkiG9w0BAQUFADBtMQswCQYDVQQGEwJT -QTEPMA0GA1UECBMGUml5YWRoMQ8wDQYDVQQHEwZSaXlhZGgxEDAOBgNVBAoTB1lv -dXR5cGUxEDAOBgNVBAsTB1lvdXR5cGUxGDAWBgNVBAMTD0x1Y2llbm5lIEFuc2Vs -YTAeFw0xNjA5MDgxNDM2MjJaFw00NDAxMjUxNDM2MjJaMG0xCzAJBgNVBAYTAlNB -MQ8wDQYDVQQIEwZSaXlhZGgxDzANBgNVBAcTBlJpeWFkaDEQMA4GA1UEChMHWW91 -dHlwZTEQMA4GA1UECxMHWW91dHlwZTEYMBYGA1UEAxMPTHVjaWVubmUgQW5zZWxh -MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsIlEcOcPEwEtBolmgcya -m0MmnBbCMynPqKxw8wXQXSpa8f2lJcN3wQN9dGj+9+N4XO7zJupKGq/9Z/IhIdlu -K3rQx8zX/Y9yFxE2ufw/t1iWOGlSZGHBa/L+jhJ7klyMCmXbjHi+AaUMDF7v8+oJ -KE2vaPg84ZeOpe9VF/LpnTyMf+GFsF9qEpuhBpJXBicmHajL8Dl1VwxPyaqj8wu2 -UIruNtnAw76gWLd9glcoyygqOWI1gW9uI8Y7ZeTOMvMTznB5VRBtlsDwVLL9msOG -ff0NHq2d+YDMs7vmPtuttwd7bgG37LMIASDatG5dr/jNeyqb9rY4TCo9R3bOgivw -VQIDAQABoyEwHzAdBgNVHQ4EFgQUEPoIQyYzpjseEK7hqm6UALvjJj8wDQYJKoZI -hvcNAQEFBQADggEBAD/C/48B4MvF2WzhMtLIAWuhtp73xBy6GCQBKT1dn9dgtXfD -LuHAvkx28CoOTso4Ia+JhWuu7jGfYdtL00ezV8d8Ma1k/SJfWyHpgDDk1MEhvn+h -tOoUQpt0S+QhKFxDm+INv2zw/P/TDIIodHQqkX+YVSLQMhUGRTq3vhDnfJqedAUr -QIhZjCZx9VjjiM4yhcabKEHpxqLQOcoeHB8zchnP1j/N+QSIW6hICqjcPLPLzpPu -M0RmEuRYz3EJ2P3jINhaCLFRLHTnoN2lVDS32v5Cr+IC7A1hPUcHG+07junRMEiG -uTYj9+UYI6phGJBABfFp7/oxs080RXCrKUhR+Go= ------END CERTIFICATE-----