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-----