فهرست منبع

Completely rewritten code.

Implement read-only zip file - class \PhpZip\ZipFile, create and update zip file - \PhpZip\ZipOutputFile.
Supports ZIP64 ext, Traditional PKWARE Encryption, WinZip AES Encryption.
Ne-Lexa 9 سال پیش
والد
کامیت
560a94c910
48فایلهای تغییر یافته به همراه7770 افزوده شده و 2890 حذف شده
  1. 363 157
      README.md
  2. 10 0
      bootstrap.xml
  3. 17 5
      composer.json
  4. 0 45
      src/Nelexa/Zip/FilterFileIterator.php
  5. 0 905
      src/Nelexa/Zip/ZipEntry.php
  6. 0 7
      src/Nelexa/Zip/ZipException.php
  7. 0 1374
      src/Nelexa/Zip/ZipFile.php
  8. 0 104
      src/Nelexa/Zip/ZipUtils.php
  9. 215 0
      src/PhpZip/Crypto/TraditionalPkwareEncryptionEngine.php
  10. 231 0
      src/PhpZip/Crypto/WinZipAesEngine.php
  11. 70 0
      src/PhpZip/Exception/Crc32Exception.php
  12. 14 0
      src/PhpZip/Exception/IllegalArgumentException.php
  13. 13 0
      src/PhpZip/Exception/ZipAuthenticationException.php
  14. 14 0
      src/PhpZip/Exception/ZipCryptoException.php
  15. 14 0
      src/PhpZip/Exception/ZipException.php
  16. 14 0
      src/PhpZip/Exception/ZipNotFoundEntry.php
  17. 14 0
      src/PhpZip/Exception/ZipUnsupportMethod.php
  18. 98 0
      src/PhpZip/Extra/DefaultExtraField.php
  19. 120 0
      src/PhpZip/Extra/ExtraField.php
  20. 21 0
      src/PhpZip/Extra/ExtraFieldHeader.php
  21. 213 0
      src/PhpZip/Extra/ExtraFields.php
  22. 176 0
      src/PhpZip/Extra/NtfsExtraField.php
  23. 236 0
      src/PhpZip/Extra/WinZipAesEntryExtraField.php
  24. 42 0
      src/PhpZip/Mapper/OffsetPositionMapper.php
  25. 29 0
      src/PhpZip/Mapper/PositionMapper.php
  26. 1187 0
      src/PhpZip/Model/ZipEntry.php
  27. 384 0
      src/PhpZip/Model/ZipInfo.php
  28. 22 0
      src/PhpZip/Output/ZipOutputEmptyDirEntry.php
  29. 46 0
      src/PhpZip/Output/ZipOutputEntry.php
  30. 54 0
      src/PhpZip/Output/ZipOutputStreamEntry.php
  31. 46 0
      src/PhpZip/Output/ZipOutputStringEntry.php
  32. 56 0
      src/PhpZip/Output/ZipOutputZipFileEntry.php
  33. 32 0
      src/PhpZip/Util/CryptoUtil.php
  34. 77 0
      src/PhpZip/Util/DateTimeConverter.php
  35. 222 0
      src/PhpZip/Util/FilesUtil.php
  36. 60 0
      src/PhpZip/Util/Iterator/IgnoreFilesFilterIterator.php
  37. 69 0
      src/PhpZip/Util/Iterator/IgnoreFilesRecursiveFilterIterator.php
  38. 44 0
      src/PhpZip/Util/PackUtil.php
  39. 30 0
      src/PhpZip/Util/StringUtil.php
  40. 115 0
      src/PhpZip/ZipConstants.php
  41. 908 0
      src/PhpZip/ZipFile.php
  42. 1400 0
      src/PhpZip/ZipOutputFile.php
  43. 1049 0
      tests/PhpZip/ZipTest.php
  44. 45 0
      tests/PhpZip/ZipTestCase.php
  45. 0 244
      tests/TestZipFile.php
  46. BIN
      tests/res/file.apk
  47. 0 28
      tests/res/private.pem
  48. 0 21
      tests/res/public.pem

+ 363 - 157
README.md

@@ -1,249 +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();
+}
 ```
-Add empty dir
+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
-$zip->addEmptyDir($dirName);
+$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
+//)
 ```
-Add dir
+Get info for all entries.
 ```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
+$zipAllInfo = $zipFile->getAllInfo();
+print_r($zipAllInfo);
+//Array
+//(
+//    [file.txt] => PhpZip\Model\ZipInfo Object
+//    (
+//            ...
+//    )
+//
+//    [file2.txt] => PhpZip\Model\ZipInfo Object
+//    (
+//            ...
+//    )
+//    
+//    ...
+//)
 ```
-Add files from glob pattern
+Extract all files to directory.
 ```php
-$zip->addGlob("music/*.mp3"); // add all mp3 files
+$zipFile->extractTo($directory);
 ```
-Add files from regex pattern
+Extract some files to directory.
 ```php
-$zip->addPattern("~file[0-9]+\.jpg$~", "picture/");
+$extractOnlyFiles = ["filename1", "filename2", "dir/dir/dir/"];
+$zipFile->extractTo($directory, $extractOnlyFiles);
 ```
-Add file
+Get entry content.
 ```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);
+$data = $zipFile->getEntryContent($entryName);
 ```
-Add file from string
+Close zip archive.
 ```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);
+$zipFile->close();
 ```
-Update timestamp for all files
+#### Class `\PhpZip\ZipOutputFile` (create, update, extract)
+Create zip archive.
 ```php
-$timestamp = time(); // now time
-$zip->updateTimestamp($timestamp);
+$zipOutputFile = new \PhpZip\ZipOutputFile();
+// or
+$zipOutputFile = \PhpZip\ZipOutputFile::create();
 ```
-Delete files from glob pattern
+Open zip file from update.
 ```php
-$zip->deleteGlob("*.jpg"); // remove all jpg files
+// 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);
 ```
-Delete files from regex pattern
+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->deletePattern("~\.jpg$~i"); // remove all jpg files
+$zipOutputFile->addEmptyDir($dirName);
 ```
-Delete file from index
+Add a directory **recursively** to the archive.
 ```php
-$zip->deleteIndex(0);
+$zipOutputFile->addDir($dirName);
+// or
+$zipOutputFile->addDir($dirName, true);
 ```
-Delete all files
+Add a directory **not recursively** to the archive.
 ```php
-$zip->deleteAll();
+$zipOutputFile->addDir($dirName, false);
 ```
-Delete from file name
+Add a directory to the archive by path `$moveToPath`
 ```php
-$zip->deleteName($filename);
+$moveToPath = 'dir/subdir/';
+$zipOutputFile->addDir($dirName, $boolResursive, $moveToPath);
 ```
-Extract zip archive
+Add a directory to the archive with ignoring files.
 ```php
-$zip->extractTo($toPath)
-$zip->extractTo($toPath, array("file1", "file2")); // extract only files file1 and file2
+$ignoreFiles = ["file_ignore.txt", "dir_ignore/sub dir ignore/"];
+$zipOutputFile->addDir($dirName, $boolResursive, $moveToPath, $ignoreFiles);
 ```
-Get archive comment
+Add a directory and set compression method.
 ```php
-$archiveComment = $zip->getArchiveComment();
+$compressionMethod = ZipEntry::METHOD_DEFLATED;
+$zipOutputFile->addDir($dirName, $boolRecursive, $moveToPath, $ignoreFiles, $compressionMethod);
 ```
-Set archive comment
+Add a files **recursively** from [glob pattern](https://en.wikipedia.org/wiki/Glob_(programming)) to the archive.
 ```php
-$zip->setArchiveComment($comment)
+$globPattern = '**.{jpg,jpeg,png,gif}'; // example glob pattern -> add all .jpg, .jpeg, .png and .gif files
+$zipOutputFile->addFilesFromGlob($inputDir, $globPattern);
 ```
-Get comment file from index
+Add a files **not recursively** from [glob pattern](https://en.wikipedia.org/wiki/Glob_(programming)) to the archive.
 ```php
-$commentFile = $zip->getCommentIndex($index);
+$recursive = false;
+$zipOutputFile->addFilesFromGlob($inputDir, $globPattern, $recursive);
 ```
-Set comment file from index
+Add a files from [glob pattern](https://en.wikipedia.org/wiki/Glob_(programming)) to the archive by path `$moveToPath`.
 ```php
-$zip->setCommentIndex($index, $comment);
+$moveToPath = 'dir/dir2/dir3';
+$zipOutputFile->addFilesFromGlob($inputDir, $globPattern, $recursive = true, $moveToPath);
 ```
-Get comment file from filename
+Add a files from [glob pattern](https://en.wikipedia.org/wiki/Glob_(programming)) to the archive and set compression method.
 ```php
-$commentFile = $zip->getCommentName($filename);
+$compressionMethod = ZipEntry::METHOD_DEFLATED;
+$zipOutputFile->addFilesFromGlob($inputDir, $globPattern, $recursive, $moveToPath, $compressionMethod);
 ```
-Set comment file from filename
+Add a files **recursively** from [RegEx (Regular Expression) pattern](https://en.wikipedia.org/wiki/Regular_expression) to the archive.
 ```php
-$zip->setCommentName($name, $comment);
+$regexPattern = '/\.(jpe?g|png|gif)$/si'; // example regex pattern -> add all .jpg, .jpeg, .png and .gif files
+$zipOutputFile->addFilesFromRegex($inputDir, $regexPattern);
 ```
-Get file content from index
+Add a files **not recursively** from [RegEx (Regular Expression) pattern](https://en.wikipedia.org/wiki/Regular_expression) to the archive.
 ```php
-$content = $zip->getFromIndex($index);
+$recursive = false;
+$zipOutputFile->addFilesFromRegex($inputDir, $regexPattern, $recursive);
 ```
-Get file content from filename
+Add a files from [RegEx (Regular Expression) pattern](https://en.wikipedia.org/wiki/Regular_expression) to the archive by path `$moveToPath`.
 ```php
-$content = $zip->getFromName($name);
+$moveToPath = 'dir/dir2/dir3';
+$zipOutputFile->addFilesFromRegex($inputDir, $regexPattern, $recursive = true, $moveToPath);
 ```
-Get filename from index
+Add a files from [RegEx (Regular Expression) pattern](https://en.wikipedia.org/wiki/Regular_expression) to the archive and set compression method.
 ```php
-$filename = $zip->getNameIndex($index);
+$compressionMethod = ZipEntry::METHOD_DEFLATED;
+$zipOutputFile->addFilesFromRegex($inputDir, $regexPattern, $recursive, $moveToPath, $compressionMethod);
 ```
-Rename file from index
+Rename entry name.
 ```php
-$zip->renameIndex($index, $newFilename);
+$zipOutputFile->rename($oldName, $newName);
 ```
-Rename file from filename
+Delete entry by name.
 ```php
-$zip->renameName($oldName, $newName);
+$zipOutputFile->deleteFromName($entryName);
 ```
-Get zip entries
+Delete entries from [glob pattern](https://en.wikipedia.org/wiki/Glob_(programming)).
 ```php
-/**
- * @var \Nelexa\Zip\ZipEntry[] $zipEntries
- */
-$zipEntries = $zip->getZipEntries();
+$globPattern = '**.{jpg,jpeg,png,gif}'; // example glob pattern -> delete all .jpg, .jpeg, .png and .gif files
+$zipOutputFile->deleteFromGlob($globPattern);
 ```
-Get zip entry from index
+Delete entries from [RegEx (Regular Expression) pattern](https://en.wikipedia.org/wiki/Regular_expression).
 ```php
-/**
- * @var \Nelexa\Zip\ZipEntry $zipEntry
- */
-$zipEntry = $zip->getZipEntryIndex($index);
+$regexPattern = '/\.(jpe?g|png|gif)$/si'; // example regex pattern -> delete all .jpg, .jpeg, .png and .gif files
+$zipOutputFile->deleteFromRegex($regexPattern);
 ```
-Get zip entry from filename
+Delete all entries.
 ```php
-/**
- * @var \Nelexa\Zip\ZipEntry $zipEntry
- */
-$zipEntry = $zip->getZipEntryName($name);
+$zipOutputFile->deleteAll();
 ```
-Get info from index
+Get num entries.
 ```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
-// ]
+$count = $zipOutputFile->count();
+// or
+$count = count($zipOutputFile);
 ```
-Get info from name
+Get list files.
 ```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
-// ]
+$listFiles = $zipOutputFile->getListFiles();
 ```
-Get info from all files
+Get the compression level for entries.
 ```php
-$info = $zip->getExtendedListFiles();
+$compressionLevel = $zipOutputFile->getLevel();
 ```
-Get output contents
+Sets the compression level for entries.
 ```php
-$content = $zip->output();
+// 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);
 ```
-Save opened file
+Get comment archive.
 ```php
-$isSuccessSave = $zip->save();
+$commentArchive = $zipOutputFile->getComment();
 ```
-Save file as
+Set comment archive.
 ```php
-$zip->saveAs($outputFile);
+$zipOutputFile->setComment($commentArchive);
 ```
-Close archive
+Get comment zip entry.
 ```php
-$zip->close();
+$commentEntry = $zipOutputFile->getEntryComment($entryName);
 ```
+Set comment zip entry.
+```php
+$zipOutputFile->setEntryComment($entryName, $entryComment);
+```
+Set compression method for zip entry.
+```php
+$compressionMethod = ZipEntry::METHOD_DEFLATED;
+$zipOutputMethod->setCompressionMethod($entryName, $compressionMethod);
+
+// Support compression methods:
+// ZipEntry::METHOD_STORED - no compression
+// ZipEntry::METHOD_DEFLATED - deflate compression
+// ZipEntry::METHOD_BZIP2 - bzip2 compression (need bz2 extension)
+```
+Set a password for all previously added entries.
+```php
+$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);
 
-### Example create zip archive
+// 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
-$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();
+$zipOutputFile->setEntryPassword($entryName, $password, $encryptionMethod);
 
-// $ 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 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();
 
-### Example modification zip archive
+    echo "Entry: $entryName" . PHP_EOL;
+    echo "Data: $dataContent" . PHP_EOL;
+    echo "-----------------------------" . PHP_EOL;
+
+    $iterator->next();
+}
+```
+Close zip archive.
 ```php
-$zip = new \Nelexa\Zip\ZipFile();
-$zip->open("output.zip");
-$zip->addFromString("new-file", file_get_contents(__FILE__));
-$zip->saveAs("output2.zip");
-$zip->close();
+$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
 
-// $ 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%
+Running Tests
+-------------
+```bash
+vendor/bin/phpunit -v --tap -c bootstrap.xml
 ```

+ 10 - 0
bootstrap.xml

@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<phpunit bootstrap="./vendor/autoload.php" colors="true">
+
+    <testsuites>
+        <testsuite name="PhpZip test suite">
+            <directory>./tests</directory>
+        </testsuite>
+    </testsuites>
+
+</phpunit>

+ 17 - 5
composer.json

@@ -1,7 +1,13 @@
 {
   "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": "4.8"
   },
@@ -9,17 +15,23 @@
   "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/Nelexa/Zip"
+      "": "src/"
+    }
+  },
+  "autoload-dev": {
+    "psr-4": {
+      "": "tests/"
     }
   }
 }

+ 0 - 45
src/Nelexa/Zip/FilterFileIterator.php

@@ -1,45 +0,0 @@
-<?php
-namespace Nelexa\Zip;
-
-class FilterFileIterator extends \FilterIterator
-{
-    private $ignoreFiles;
-    private static $ignoreAlways = array('..');
-
-    /**
-     * @param \Iterator $iterator
-     * @param array $ignoreFiles
-     */
-    public function __construct(\Iterator $iterator, array $ignoreFiles)
-    {
-        parent::__construct($iterator);
-        $this->ignoreFiles = array_merge(self::$ignoreAlways, $ignoreFiles);
-    }
-
-    /**
-     * (PHP 5 &gt;= 5.1.0)<br/>
-     * 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);
-    }
-}

+ 0 - 905
src/Nelexa/Zip/ZipEntry.php

@@ -1,905 +0,0 @@
-<?php
-namespace Nelexa\Zip;
-
-use Nelexa\Buffer\Buffer;
-use Nelexa\Buffer\StringBuffer;
-
-class ZipEntry
-{
-    // made by constants
-    const MADE_BY_MS_DOS = 0;
-    const MADE_BY_AMIGA = 1;
-    const MADE_BY_OPEN_VMS = 2;
-    const MADE_BY_UNIX = 3;
-    const MADE_BY_VM_CMS = 4;
-    const MADE_BY_ATARI = 5;
-    const MADE_BY_OS_2 = 6;
-    const MADE_BY_MACINTOSH = 7;
-    const MADE_BY_Z_SYSTEM = 8;
-    const MADE_BY_CP_M = 9;
-    const MADE_BY_WINDOWS_NTFS = 10;
-    const MADE_BY_MVS = 11;
-    const MADE_BY_VSE = 12;
-    const MADE_BY_ACORN_RISC = 13;
-    const MADE_BY_VFAT = 14;
-    const MADE_BY_ALTERNATE_MVS = 15;
-    const MADE_BY_BEOS = 16;
-    const MADE_BY_TANDEM = 17;
-    const MADE_BY_OS_400 = 18;
-    const MADE_BY_OS_X = 19;
-    const MADE_BY_UNKNOWN = 20;
-
-    private static $valuesMadeBy = array(
-        self::MADE_BY_MS_DOS => '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 . '"' .
-        '}';
-    }
-}

+ 0 - 7
src/Nelexa/Zip/ZipException.php

@@ -1,7 +0,0 @@
-<?php
-namespace Nelexa\Zip;
-
-class ZipException extends \Exception
-{
-
-}

+ 0 - 1374
src/Nelexa/Zip/ZipFile.php

@@ -1,1374 +0,0 @@
-<?php
-namespace Nelexa\Zip;
-
-use Nelexa\Buffer\Buffer;
-use Nelexa\Buffer\BufferException;
-use Nelexa\Buffer\MathHelper;
-use Nelexa\Buffer\MemoryResourceBuffer;
-use Nelexa\Buffer\ResourceBuffer;
-use Nelexa\Buffer\StringBuffer;
-
-class ZipFile
-{
-    const SIGNATURE_LOCAL_HEADER = 0x04034b50;
-    const SIGNATURE_CENTRAL_DIR = 0x02014b50;
-    const SIGNATURE_END_CENTRAL_DIR = 0x06054b50;
-
-    private static $initPwdKeys = array(305419896, 591751049, 878082192);
-
-
-    /**
-     * @var string
-     */
-    private $filename;
-    /**
-     * @var Buffer
-     */
-    private $buffer;
-    /**
-     * @var int
-     */
-    private $offsetCentralDirectory;
-    /**
-     * @var int
-     */
-    private $sizeCentralDirectory = 0;
-    /**
-     * @var ZipEntry[]
-     */
-    private $zipEntries;
-    /**
-     * @var string[]
-     */
-    private $zipEntriesIndex;
-    /**
-     * @var string
-     */
-    private $zipComment = "";
-    /**
-     * @var string
-     */
-    private $password = null;
-
-    public function __construct()
-    {
-
-    }
-
-    /**
-     * Create zip archive
-     */
-    public function create()
-    {
-        $this->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;
-    }
-
-}

+ 0 - 104
src/Nelexa/Zip/ZipUtils.php

@@ -1,104 +0,0 @@
-<?php
-namespace Nelexa\Zip;
-
-use Nelexa\Buffer\MathHelper;
-
-class ZipUtils
-{
-    const DECRYPT_HEADER_SIZE = 12;
-    public static $CFH_SIGNATURE = array(0x50, 0x4b, 0x01, 0x02);
-    public static $LFH_SIGNATURE = array(0x50, 0x4b, 0x03, 0x04);
-    public static $ECD_SIGNATURE = array(0x50, 0x4b, 0x05, 0x06);
-    public static $DD_SIGNATURE = array(0x50, 0x4b, 0x07, 0x08);
-
-    private static $CRC_TABLE = array(
-        0x00000000, 0x77073096, 0xEE0E612C, 0x990951BA, 0x076DC419, 0x706AF48F,
-        0xE963A535, 0x9E6495A3, 0x0EDB8832, 0x79DCB8A4, 0xE0D5E91E, 0x97D2D988,
-        0x09B64C2B, 0x7EB17CBD, 0xE7B82D07, 0x90BF1D91, 0x1DB71064, 0x6AB020F2,
-        0xF3B97148, 0x84BE41DE, 0x1ADAD47D, 0x6DDDE4EB, 0xF4D4B551, 0x83D385C7,
-        0x136C9856, 0x646BA8C0, 0xFD62F97A, 0x8A65C9EC, 0x14015C4F, 0x63066CD9,
-        0xFA0F3D63, 0x8D080DF5, 0x3B6E20C8, 0x4C69105E, 0xD56041E4, 0xA2677172,
-        0x3C03E4D1, 0x4B04D447, 0xD20D85FD, 0xA50AB56B, 0x35B5A8FA, 0x42B2986C,
-        0xDBBBC9D6, 0xACBCF940, 0x32D86CE3, 0x45DF5C75, 0xDCD60DCF, 0xABD13D59,
-        0x26D930AC, 0x51DE003A, 0xC8D75180, 0xBFD06116, 0x21B4F4B5, 0x56B3C423,
-        0xCFBA9599, 0xB8BDA50F, 0x2802B89E, 0x5F058808, 0xC60CD9B2, 0xB10BE924,
-        0x2F6F7C87, 0x58684C11, 0xC1611DAB, 0xB6662D3D, 0x76DC4190, 0x01DB7106,
-        0x98D220BC, 0xEFD5102A, 0x71B18589, 0x06B6B51F, 0x9FBFE4A5, 0xE8B8D433,
-        0x7807C9A2, 0x0F00F934, 0x9609A88E, 0xE10E9818, 0x7F6A0DBB, 0x086D3D2D,
-        0x91646C97, 0xE6635C01, 0x6B6B51F4, 0x1C6C6162, 0x856530D8, 0xF262004E,
-        0x6C0695ED, 0x1B01A57B, 0x8208F4C1, 0xF50FC457, 0x65B0D9C6, 0x12B7E950,
-        0x8BBEB8EA, 0xFCB9887C, 0x62DD1DDF, 0x15DA2D49, 0x8CD37CF3, 0xFBD44C65,
-        0x4DB26158, 0x3AB551CE, 0xA3BC0074, 0xD4BB30E2, 0x4ADFA541, 0x3DD895D7,
-        0xA4D1C46D, 0xD3D6F4FB, 0x4369E96A, 0x346ED9FC, 0xAD678846, 0xDA60B8D0,
-        0x44042D73, 0x33031DE5, 0xAA0A4C5F, 0xDD0D7CC9, 0x5005713C, 0x270241AA,
-        0xBE0B1010, 0xC90C2086, 0x5768B525, 0x206F85B3, 0xB966D409, 0xCE61E49F,
-        0x5EDEF90E, 0x29D9C998, 0xB0D09822, 0xC7D7A8B4, 0x59B33D17, 0x2EB40D81,
-        0xB7BD5C3B, 0xC0BA6CAD, 0xEDB88320, 0x9ABFB3B6, 0x03B6E20C, 0x74B1D29A,
-        0xEAD54739, 0x9DD277AF, 0x04DB2615, 0x73DC1683, 0xE3630B12, 0x94643B84,
-        0x0D6D6A3E, 0x7A6A5AA8, 0xE40ECF0B, 0x9309FF9D, 0x0A00AE27, 0x7D079EB1,
-        0xF00F9344, 0x8708A3D2, 0x1E01F268, 0x6906C2FE, 0xF762575D, 0x806567CB,
-        0x196C3671, 0x6E6B06E7, 0xFED41B76, 0x89D32BE0, 0x10DA7A5A, 0x67DD4ACC,
-        0xF9B9DF6F, 0x8EBEEFF9, 0x17B7BE43, 0x60B08ED5, 0xD6D6A3E8, 0xA1D1937E,
-        0x38D8C2C4, 0x4FDFF252, 0xD1BB67F1, 0xA6BC5767, 0x3FB506DD, 0x48B2364B,
-        0xD80D2BDA, 0xAF0A1B4C, 0x36034AF6, 0x41047A60, 0xDF60EFC3, 0xA867DF55,
-        0x316E8EEF, 0x4669BE79, 0xCB61B38C, 0xBC66831A, 0x256FD2A0, 0x5268E236,
-        0xCC0C7795, 0xBB0B4703, 0x220216B9, 0x5505262F, 0xC5BA3BBE, 0xB2BD0B28,
-        0x2BB45A92, 0x5CB36A04, 0xC2D7FFA7, 0xB5D0CF31, 0x2CD99E8B, 0x5BDEAE1D,
-        0x9B64C2B0, 0xEC63F226, 0x756AA39C, 0x026D930A, 0x9C0906A9, 0xEB0E363F,
-        0x72076785, 0x05005713, 0x95BF4A82, 0xE2B87A14, 0x7BB12BAE, 0x0CB61B38,
-        0x92D28E9B, 0xE5D5BE0D, 0x7CDCEFB7, 0x0BDBDF21, 0x86D3D2D4, 0xF1D4E242,
-        0x68DDB3F8, 0x1FDA836E, 0x81BE16CD, 0xF6B9265B, 0x6FB077E1, 0x18B74777,
-        0x88085AE6, 0xFF0F6A70, 0x66063BCA, 0x11010B5C, 0x8F659EFF, 0xF862AE69,
-        0x616BFFD3, 0x166CCF45, 0xA00AE278, 0xD70DD2EE, 0x4E048354, 0x3903B3C2,
-        0xA7672661, 0xD06016F7, 0x4969474D, 0x3E6E77DB, 0xAED16A4A, 0xD9D65ADC,
-        0x40DF0B66, 0x37D83BF0, 0xA9BCAE53, 0xDEBB9EC5, 0x47B2CF7F, 0x30B5FFE9,
-        0xBDBDF21C, 0xCABAC28A, 0x53B39330, 0x24B4A3A6, 0xBAD03605, 0xCDD70693,
-        0x54DE5729, 0x23D967BF, 0xB3667A2E, 0xC4614AB8, 0x5D681B02, 0x2A6F2B94,
-        0xB40BBE37, 0xC30C8EA1, 0x5A05DF1B, 0x2D02EF8D,
-    );
-
-    /**
-     * @param int $charAt
-     * @param int[] $keys
-     * @return array
-     */
-    public static function updateKeys($charAt, array $keys)
-    {
-        $keys[0] = self::crc32($keys[0], $charAt);
-        $keys[1] = MathHelper::add($keys[1], MathHelper::bitwiseAnd($keys[0], 0xff));
-        $keys[1] = MathHelper::castToInt(MathHelper::add(MathHelper::mul($keys[1], 134775813), 1));
-        $keys[2] = self::crc32($keys[2], MathHelper::rightShift32($keys[1], 24));
-        return $keys;
-    }
-
-    /**
-     * Alg: ((oldCrc >>> 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";
-    }
-
-
-}

+ 215 - 0
src/PhpZip/Crypto/TraditionalPkwareEncryptionEngine.php

@@ -0,0 +1,215 @@
+<?php
+namespace PhpZip\Crypto;
+
+use PhpZip\Exception\ZipAuthenticationException;
+use PhpZip\Model\ZipEntry;
+use PhpZip\Util\CryptoUtil;
+
+/**
+ * Traditional PKWARE Encryption Engine.
+ *
+ * @see https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT .ZIP File Format Specification
+ * @author Ne-Lexa alexey@nelexa.ru
+ * @license MIT
+ */
+class TraditionalPkwareEncryptionEngine
+{
+    /**
+     * Encryption header size
+     */
+    const STD_DEC_HDR_SIZE = 12;
+
+    /**
+     * Crc table
+     *
+     * @var array
+     */
+    private static $CRC_TABLE = [
+        0x00000000, 0x77073096, 0xee0e612c, 0x990951ba, 0x076dc419, 0x706af48f, 0xe963a535, 0x9e6495a3,
+        0x0edb8832, 0x79dcb8a4, 0xe0d5e91e, 0x97d2d988, 0x09b64c2b, 0x7eb17cbd, 0xe7b82d07, 0x90bf1d91,
+        0x1db71064, 0x6ab020f2, 0xf3b97148, 0x84be41de, 0x1adad47d, 0x6ddde4eb, 0xf4d4b551, 0x83d385c7,
+        0x136c9856, 0x646ba8c0, 0xfd62f97a, 0x8a65c9ec, 0x14015c4f, 0x63066cd9, 0xfa0f3d63, 0x8d080df5,
+        0x3b6e20c8, 0x4c69105e, 0xd56041e4, 0xa2677172, 0x3c03e4d1, 0x4b04d447, 0xd20d85fd, 0xa50ab56b,
+        0x35b5a8fa, 0x42b2986c, 0xdbbbc9d6, 0xacbcf940, 0x32d86ce3, 0x45df5c75, 0xdcd60dcf, 0xabd13d59,
+        0x26d930ac, 0x51de003a, 0xc8d75180, 0xbfd06116, 0x21b4f4b5, 0x56b3c423, 0xcfba9599, 0xb8bda50f,
+        0x2802b89e, 0x5f058808, 0xc60cd9b2, 0xb10be924, 0x2f6f7c87, 0x58684c11, 0xc1611dab, 0xb6662d3d,
+        0x76dc4190, 0x01db7106, 0x98d220bc, 0xefd5102a, 0x71b18589, 0x06b6b51f, 0x9fbfe4a5, 0xe8b8d433,
+        0x7807c9a2, 0x0f00f934, 0x9609a88e, 0xe10e9818, 0x7f6a0dbb, 0x086d3d2d, 0x91646c97, 0xe6635c01,
+        0x6b6b51f4, 0x1c6c6162, 0x856530d8, 0xf262004e, 0x6c0695ed, 0x1b01a57b, 0x8208f4c1, 0xf50fc457,
+        0x65b0d9c6, 0x12b7e950, 0x8bbeb8ea, 0xfcb9887c, 0x62dd1ddf, 0x15da2d49, 0x8cd37cf3, 0xfbd44c65,
+        0x4db26158, 0x3ab551ce, 0xa3bc0074, 0xd4bb30e2, 0x4adfa541, 0x3dd895d7, 0xa4d1c46d, 0xd3d6f4fb,
+        0x4369e96a, 0x346ed9fc, 0xad678846, 0xda60b8d0, 0x44042d73, 0x33031de5, 0xaa0a4c5f, 0xdd0d7cc9,
+        0x5005713c, 0x270241aa, 0xbe0b1010, 0xc90c2086, 0x5768b525, 0x206f85b3, 0xb966d409, 0xce61e49f,
+        0x5edef90e, 0x29d9c998, 0xb0d09822, 0xc7d7a8b4, 0x59b33d17, 0x2eb40d81, 0xb7bd5c3b, 0xc0ba6cad,
+        0xedb88320, 0x9abfb3b6, 0x03b6e20c, 0x74b1d29a, 0xead54739, 0x9dd277af, 0x04db2615, 0x73dc1683,
+        0xe3630b12, 0x94643b84, 0x0d6d6a3e, 0x7a6a5aa8, 0xe40ecf0b, 0x9309ff9d, 0x0a00ae27, 0x7d079eb1,
+        0xf00f9344, 0x8708a3d2, 0x1e01f268, 0x6906c2fe, 0xf762575d, 0x806567cb, 0x196c3671, 0x6e6b06e7,
+        0xfed41b76, 0x89d32be0, 0x10da7a5a, 0x67dd4acc, 0xf9b9df6f, 0x8ebeeff9, 0x17b7be43, 0x60b08ed5,
+        0xd6d6a3e8, 0xa1d1937e, 0x38d8c2c4, 0x4fdff252, 0xd1bb67f1, 0xa6bc5767, 0x3fb506dd, 0x48b2364b,
+        0xd80d2bda, 0xaf0a1b4c, 0x36034af6, 0x41047a60, 0xdf60efc3, 0xa867df55, 0x316e8eef, 0x4669be79,
+        0xcb61b38c, 0xbc66831a, 0x256fd2a0, 0x5268e236, 0xcc0c7795, 0xbb0b4703, 0x220216b9, 0x5505262f,
+        0xc5ba3bbe, 0xb2bd0b28, 0x2bb45a92, 0x5cb36a04, 0xc2d7ffa7, 0xb5d0cf31, 0x2cd99e8b, 0x5bdeae1d,
+        0x9b64c2b0, 0xec63f226, 0x756aa39c, 0x026d930a, 0x9c0906a9, 0xeb0e363f, 0x72076785, 0x05005713,
+        0x95bf4a82, 0xe2b87a14, 0x7bb12bae, 0x0cb61b38, 0x92d28e9b, 0xe5d5be0d, 0x7cdcefb7, 0x0bdbdf21,
+        0x86d3d2d4, 0xf1d4e242, 0x68ddb3f8, 0x1fda836e, 0x81be16cd, 0xf6b9265b, 0x6fb077e1, 0x18b74777,
+        0x88085ae6, 0xff0f6a70, 0x66063bca, 0x11010b5c, 0x8f659eff, 0xf862ae69, 0x616bffd3, 0x166ccf45,
+        0xa00ae278, 0xd70dd2ee, 0x4e048354, 0x3903b3c2, 0xa7672661, 0xd06016f7, 0x4969474d, 0x3e6e77db,
+        0xaed16a4a, 0xd9d65adc, 0x40df0b66, 0x37d83bf0, 0xa9bcae53, 0xdebb9ec5, 0x47b2cf7f, 0x30b5ffe9,
+        0xbdbdf21c, 0xcabac28a, 0x53b39330, 0x24b4a3a6, 0xbad03605, 0xcdd70693, 0x54de5729, 0x23d967bf,
+        0xb3667a2e, 0xc4614ab8, 0x5d681b02, 0x2a6f2b94, 0xb40bbe37, 0xc30c8ea1, 0x5a05df1b, 0x2d02ef8d,
+    ];
+
+    /**
+     * Encryption keys
+     *
+     * @var array
+     */
+    private $keys = [];
+
+    /**
+     * @var ZipEntry
+     */
+    private $entry;
+
+    /**
+     * ZipCryptoEngine constructor.
+     *
+     * @param ZipEntry $entry
+     */
+    public function __construct(ZipEntry $entry)
+    {
+        $this->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;
+    }
+}

+ 231 - 0
src/PhpZip/Crypto/WinZipAesEngine.php

@@ -0,0 +1,231 @@
+<?php
+namespace PhpZip\Crypto;
+
+use PhpZip\Exception\ZipAuthenticationException;
+use PhpZip\Exception\ZipCryptoException;
+use PhpZip\Extra\WinZipAesEntryExtraField;
+use PhpZip\Model\ZipEntry;
+use PhpZip\Util\CryptoUtil;
+
+/**
+ * WinZip Aes Encryption Engine.
+ *
+ * @author Ne-Lexa alexey@nelexa.ru
+ * @license MIT
+ */
+class WinZipAesEngine
+{
+    /**
+     * The block size of the Advanced Encryption Specification (AES) Algorithm
+     * in bits (AES_BLOCK_SIZE_BITS).
+     */
+    const AES_BLOCK_SIZE_BITS = 128;
+    const PWD_VERIFIER_BITS = 16;
+    /**
+     * The iteration count for the derived keys of the cipher, KLAC and MAC.
+     */
+    const ITERATION_COUNT = 1000;
+    /**
+     * @var ZipEntry
+     */
+    private $entry;
+
+    /**
+     * WinZipAesEngine constructor.
+     * @param ZipEntry $entry
+     */
+    public function __construct(ZipEntry $entry)
+    {
+        $this->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)
+        );
+    }
+}

+ 70 - 0
src/PhpZip/Exception/Crc32Exception.php

@@ -0,0 +1,70 @@
+<?php
+namespace PhpZip\Exception;
+
+/**
+ * Thrown to indicate a CRC32 mismatch between the declared value in the
+ * Central File Header and the Data Descriptor or between the declared value
+ * and the computed value from the decompressed data.
+ *
+ * The exception's detail message is the name of the ZIP entry.
+ *
+ * @author Ne-Lexa alexey@nelexa.ru
+ * @license MIT
+ */
+class Crc32Exception extends ZipException
+{
+    /**
+     * Expected crc.
+     *
+     * @var int
+     */
+    private $expectedCrc;
+
+    /**
+     * Actual crc.
+     *
+     * @var int
+     */
+    private $actualCrc;
+
+    /**
+     * Crc32Exception constructor.
+     *
+     * @param string $name
+     * @param int $expected
+     * @param int $actual
+     */
+    public function __construct($name, $expected, $actual)
+    {
+        parent::__construct($name
+            . " (expected CRC32 value 0x"
+            . dechex($expected)
+            . ", but is actually 0x"
+            . dechex($actual)
+            . ")");
+        assert($expected != $actual);
+        $this->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;
+    }
+
+}

+ 14 - 0
src/PhpZip/Exception/IllegalArgumentException.php

@@ -0,0 +1,14 @@
+<?php
+namespace PhpZip\Exception;
+
+/**
+ * Thrown to indicate that a method has been passed an illegal or
+ * inappropriate argument.
+ *
+ * @author Ne-Lexa alexey@nelexa.ru
+ * @license MIT
+ */
+class IllegalArgumentException extends ZipException
+{
+
+}

+ 13 - 0
src/PhpZip/Exception/ZipAuthenticationException.php

@@ -0,0 +1,13 @@
+<?php
+namespace PhpZip\Exception;
+
+/**
+ * Thrown to indicate that an authenticated ZIP entry has been tampered with.
+ *
+ * @author Ne-Lexa alexey@nelexa.ru
+ * @license MIT
+ */
+class ZipAuthenticationException extends ZipCryptoException
+{
+
+}

+ 14 - 0
src/PhpZip/Exception/ZipCryptoException.php

@@ -0,0 +1,14 @@
+<?php
+namespace PhpZip\Exception;
+
+/**
+ * Thrown if there is an issue when reading or writing an encrypted ZIP file
+ * or entry.
+ *
+ * @author Ne-Lexa alexey@nelexa.ru
+ * @license MIT
+ */
+class ZipCryptoException extends ZipException
+{
+
+}

+ 14 - 0
src/PhpZip/Exception/ZipException.php

@@ -0,0 +1,14 @@
+<?php
+namespace PhpZip\Exception;
+
+/**
+ * Signals that a Zip exception of some sort has occurred.
+ *
+ * @author Ne-Lexa alexey@nelexa.ru
+ * @license MIT
+ * @see \Exception
+ */
+class ZipException extends \Exception
+{
+
+}

+ 14 - 0
src/PhpZip/Exception/ZipNotFoundEntry.php

@@ -0,0 +1,14 @@
+<?php
+namespace PhpZip\Exception;
+
+/**
+ * Thrown if entry not found.
+ *
+ * @author Ne-Lexa alexey@nelexa.ru
+ * @license MIT
+ * @see \Exception
+ */
+class ZipNotFoundEntry extends ZipException
+{
+
+}

+ 14 - 0
src/PhpZip/Exception/ZipUnsupportMethod.php

@@ -0,0 +1,14 @@
+<?php
+namespace PhpZip\Exception;
+
+/**
+ * Thrown if entry unsupport compression method.
+ *
+ * @author Ne-Lexa alexey@nelexa.ru
+ * @license MIT
+ * @see \Exception
+ */
+class ZipUnsupportMethod extends ZipException
+{
+
+}

+ 98 - 0
src/PhpZip/Extra/DefaultExtraField.php

@@ -0,0 +1,98 @@
+<?php
+namespace PhpZip\Extra;
+
+use PhpZip\Exception\ZipException;
+
+/**
+ * Default implementation for an Extra Field in a Local or Central Header of a
+ * ZIP file.
+ *
+ * @author Ne-Lexa alexey@nelexa.ru
+ * @license MIT
+ */
+class DefaultExtraField extends ExtraField
+{
+    /**
+     * @var int
+     */
+    private static $headerId;
+
+    /**
+     * @var string
+     */
+    private $data;
+
+    /**
+     * Constructs a new Extra Field.
+     *
+     * @param int $headerId an unsigned short integer (two bytes) indicating the
+     *         type of the Extra Field.
+     * @throws ZipException
+     */
+    public function __construct($headerId)
+    {
+        if (0x0000 > $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);
+        }
+    }
+}

+ 120 - 0
src/PhpZip/Extra/ExtraField.php

@@ -0,0 +1,120 @@
+<?php
+namespace PhpZip\Extra;
+
+use PhpZip\Exception\ZipException;
+
+/**
+ * Abstract base class for an Extra Field in a Local or Central Header of a
+ * ZIP archive.
+ * It defines the common properties of all Extra Fields and how to
+ * serialize/deserialize them to/from byte arrays.
+ *
+ * @author Ne-Lexa alexey@nelexa.ru
+ * @license MIT
+ */
+abstract class ExtraField implements ExtraFieldHeader
+{
+    /** The Header ID for a ZIP64 Extended Information Extra Field. */
+    const ZIP64_HEADER_ID = 0x0001;
+
+    /**
+     * @var array|null
+     */
+    private static $registry;
+
+    /**
+     * A static factory method which creates a new Extra Field based on the
+     * given Header ID.
+     * The returned Extra Field still requires proper initialization, for
+     * example by calling ExtraField::readFrom.
+     *
+     * @param int $headerId An unsigned short integer (two bytes) which indicates
+     *         the type of the returned Extra Field.
+     * @return ExtraField A new Extra Field or null if not support header id.
+     * @throws ZipException If headerId is out of range.
+     */
+    public static function create($headerId)
+    {
+        if (0x0000 > $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);
+}

+ 21 - 0
src/PhpZip/Extra/ExtraFieldHeader.php

@@ -0,0 +1,21 @@
+<?php
+namespace PhpZip\Extra;
+
+/**
+ * Interface ExtraFieldHeader
+ *
+ * @author Ne-Lexa alexey@nelexa.ru
+ * @license MIT
+ */
+interface ExtraFieldHeader
+{
+    /**
+     * 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();
+
+}

+ 213 - 0
src/PhpZip/Extra/ExtraFields.php

@@ -0,0 +1,213 @@
+<?php
+namespace PhpZip\Extra;
+
+
+use PhpZip\Exception\ZipException;
+
+/**
+ * Represents a collection of Extra Fields as they may
+ * be present at several locations in ZIP files.
+ *
+ * @author Ne-Lexa alexey@nelexa.ru
+ * @license MIT
+ */
+class ExtraFields
+{
+    /**
+     * The map of Extra Fields.
+     * Maps from Header ID to Extra Field.
+     * Must not be null, but may be empty if no Extra Fields are used.
+     * The map is sorted by Header IDs in ascending order.
+     *
+     * @var ExtraField[]
+     */
+    private $extra = [];
+
+    /**
+     * Returns the number of Extra Fields in this collection.
+     *
+     * @return int
+     */
+    public function size()
+    {
+        return sizeof($this->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;
+        }
+    }
+
+}

+ 176 - 0
src/PhpZip/Extra/NtfsExtraField.php

@@ -0,0 +1,176 @@
+<?php
+namespace PhpZip\Extra;
+
+use PhpZip\Exception\ZipException;
+use PhpZip\Util\PackUtil;
+
+/**
+ * NTFS Extra Field
+ *
+ * @see https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT .ZIP File Format Specification
+ * @author Ne-Lexa alexey@nelexa.ru
+ * @license MIT
+ */
+class NtfsExtraField extends ExtraField
+{
+
+    /**
+     * Modify time
+     *
+     * @var int Unix Timestamp
+     */
+    private $mtime;
+
+    /**
+     * Access Time
+     *
+     * @var int Unix Timestamp
+     */
+    private $atime;
+
+    /**
+     * Create Time
+     *
+     * @var int Unix Time
+     */
+    private $ctime;
+
+    /**
+     * @var string
+     */
+    private $rawData = "";
+
+    /**
+     * 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 0x000a;
+    }
+
+    /**
+     * 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 8 * 4 + strlen($this->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;
+    }
+
+}

+ 236 - 0
src/PhpZip/Extra/WinZipAesEntryExtraField.php

@@ -0,0 +1,236 @@
+<?php
+namespace PhpZip\Extra;
+
+use PhpZip\Exception\ZipException;
+
+/**
+ * WinZip AES Extra Field.
+ *
+ * @see http://www.winzip.com/win/en/aes_info.htm AES Encryption Information: Encryption Specification AE-1 and AE-2 (WinZip Computing, S.L.)
+ * @see http://www.winzip.com/win/en/aes_tips.htm AES Coding Tips for Developers (WinZip Computing, S.L.)
+ * @author Ne-Lexa alexey@nelexa.ru
+ * @license MIT
+ */
+class WinZipAesEntryExtraField extends ExtraField
+{
+    const DATA_SIZE = 7;
+    const VENDOR_ID = 17729; // 'A' | ('E' << 8);
+
+    /**
+     * Entries of this type <em>do</em> 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 <em>not</em> 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));
+    }
+}

+ 42 - 0
src/PhpZip/Mapper/OffsetPositionMapper.php

@@ -0,0 +1,42 @@
+<?php
+namespace PhpZip\Mapper;
+
+/**
+ * Adds a offset value to the given position.
+ *
+ * @author Ne-Lexa alexey@nelexa.ru
+ * @license MIT
+ */
+class OffsetPositionMapper extends PositionMapper
+{
+    /**
+     * @var int
+     */
+    private $offset;
+
+    /**
+     * @param int $offset
+     */
+    public function __construct($offset)
+    {
+        $this->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;
+    }
+}

+ 29 - 0
src/PhpZip/Mapper/PositionMapper.php

@@ -0,0 +1,29 @@
+<?php
+namespace PhpZip\Mapper;
+
+/**
+ * Maps a given position.
+ *
+ * @author Ne-Lexa alexey@nelexa.ru
+ * @license MIT
+ */
+class PositionMapper
+{
+    /**
+     * @param int $position
+     * @return int
+     */
+    public function map($position)
+    {
+        return $position;
+    }
+
+    /**
+     * @param int $position
+     * @return int
+     */
+    public function unmap($position)
+    {
+        return $position;
+    }
+}

+ 1187 - 0
src/PhpZip/Model/ZipEntry.php

@@ -0,0 +1,1187 @@
+<?php
+namespace PhpZip\Model;
+
+use PhpZip\Exception\ZipException;
+use PhpZip\Extra\DefaultExtraField;
+use PhpZip\Extra\ExtraField;
+use PhpZip\Extra\ExtraFields;
+use PhpZip\Extra\WinZipAesEntryExtraField;
+use PhpZip\Util\DateTimeConverter;
+use PhpZip\Util\PackUtil;
+
+/**
+ * This class is used to represent a ZIP file entry.
+ *
+ * @see https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT .ZIP File Format Specification
+ * @author Ne-Lexa alexey@nelexa.ru
+ * @license MIT
+ */
+class ZipEntry
+{
+    // Bit masks for initialized fields.
+    const BIT_PLATFORM = 1,
+        BIT_METHOD = 2 /* 1 << 1 */,
+        BIT_CRC = 2 /* 1 << 2 */,
+        BIT_DATE_TIME = 64 /* 1 << 6 */,
+        BIT_EXTERNAL_ATTR = 128 /* 1 << 7*/
+    ;
+
+    /** The unknown value for numeric properties. */
+    const UNKNOWN = -1;
+
+    /** Windows platform. */
+    const PLATFORM_FAT = 0;
+
+    /** Unix platform. */
+    const PLATFORM_UNIX = 3;
+
+    /** MacOS platform */
+    const PLATFORM_OS_X = 19;
+
+    /**
+     * Method for Stored (uncompressed) entries.
+     *
+     * @see ZipEntry::setMethod()
+     */
+    const METHOD_STORED = 0;
+
+    /**
+     * Method for Deflated compressed entries.
+     *
+     * @see ZipEntry::setMethod()
+     */
+    const METHOD_DEFLATED = 8;
+
+    /**
+     * Method for BZIP2 compressed entries.
+     * Require php extension bz2.
+     *
+     * @see ZipEntry::setMethod()
+     */
+    const METHOD_BZIP2 = 12;
+
+    /**
+     * Pseudo compression method for WinZip AES encrypted entries.
+     * Require php extension openssl or mcrypt.
+     */
+    const WINZIP_AES = 99;
+
+    /** General Purpose Bit Flag mask for encrypted data. */
+    const GPBF_ENCRYPTED = 1;
+
+    /** General Purpose Bit Flag mask for data descriptor. */
+    const GPBF_DATA_DESCRIPTOR = 8; // 1 << 3;
+
+    /** General Purpose Bit Flag mask for UTF-8. */
+    const GPBF_UTF8 = 2048; // 1 << 11;
+
+    /**
+     * No specified method for set encryption method to Traditional PKWARE encryption.
+     */
+    const ENCRYPTION_METHOD_TRADITIONAL = 0;
+
+    /**
+     * No specified method for set encryption method to WinZip AES encryption.
+     */
+    const ENCRYPTION_METHOD_WINZIP_AES = 1;
+
+    /**
+     * bit flags for init state
+     *
+     * @var int
+     */
+    private $init;
+
+    /**
+     * Entry name (filename in archive)
+     *
+     * @var string
+     */
+    private $name;
+
+    /**
+     * Made by platform
+     *
+     * @var int
+     */
+    private $platform;
+
+    /**
+     * @var 2 bytes unsigned int
+     *
+     * @var int
+     */
+    private $general;
+
+    /**
+     * Compression method
+     *
+     * @var int
+     */
+    private $method;
+
+    /**
+     * Dos time
+     *
+     * @var int 4 bytes unsigned int
+     */
+    private $dosTime;
+
+    /**
+     * Crc32
+     *
+     * @var int
+     */
+    private $crc;
+
+    /**
+     * Compressed size
+     *
+     * @var int
+     */
+    private $compressedSize = self::UNKNOWN;
+
+    /**
+     * Uncompressed size
+     *
+     * @var int
+     */
+    private $size = self::UNKNOWN;
+
+    /**
+     * External attributes
+     *
+     * @var int
+     */
+    private $externalAttributes;
+
+    /**
+     * Relative Offset Of Local File Header.
+     *
+     * @var int
+     */
+    private $offset = self::UNKNOWN;
+
+    /**
+     * The map of Extra Fields.
+     * Maps from Header ID [Integer] to Extra Field [ExtraField].
+     * Should be null or may be empty if no Extra Fields are used.
+     *
+     * @var ExtraFields
+     */
+    private $fields;
+
+    /**
+     * Comment field.
+     *
+     * @var string
+     */
+    private $comment;
+
+    /**
+     * Entry password for read or write encryption data.
+     *
+     * @var string
+     */
+    private $password;
+
+    /**
+     * Encryption method.
+     *
+     * @see ZipEntry::ENCRYPTION_METHOD_TRADITIONAL
+     * @see ZipEntry::ENCRYPTION_METHOD_WINZIP_AES
+     * @var int
+     */
+    private $encryptionMethod = self::ENCRYPTION_METHOD_TRADITIONAL;
+
+    /**
+     * ZipEntry constructor.
+     *
+     * @param string $name
+     * @throws ZipException
+     */
+    public function __construct($name)
+    {
+        $this->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);
+    }
+
+}

+ 384 - 0
src/PhpZip/Model/ZipInfo.php

@@ -0,0 +1,384 @@
+<?php
+namespace PhpZip\Model;
+
+use PhpZip\Extra\NtfsExtraField;
+use PhpZip\Extra\WinZipAesEntryExtraField;
+use PhpZip\Util\FilesUtil;
+
+/**
+ * Zip info
+ *
+ * @author Ne-Lexa alexey@nelexa.ru
+ * @license MIT
+ */
+class ZipInfo
+{
+    // made by constants
+    const MADE_BY_MS_DOS = 0;
+    const MADE_BY_AMIGA = 1;
+    const MADE_BY_OPEN_VMS = 2;
+    const MADE_BY_UNIX = 3;
+    const MADE_BY_VM_CMS = 4;
+    const MADE_BY_ATARI = 5;
+    const MADE_BY_OS_2 = 6;
+    const MADE_BY_MACINTOSH = 7;
+    const MADE_BY_Z_SYSTEM = 8;
+    const MADE_BY_CP_M = 9;
+    const MADE_BY_WINDOWS_NTFS = 10;
+    const MADE_BY_MVS = 11;
+    const MADE_BY_VSE = 12;
+    const MADE_BY_ACORN_RISC = 13;
+    const MADE_BY_VFAT = 14;
+    const MADE_BY_ALTERNATE_MVS = 15;
+    const MADE_BY_BEOS = 16;
+    const MADE_BY_TANDEM = 17;
+    const MADE_BY_OS_400 = 18;
+    const MADE_BY_OS_X = 19;
+    const MADE_BY_UNKNOWN = 20;
+
+    private static $valuesMadeBy = [
+        self::MADE_BY_MS_DOS => '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()
+        . '}';
+    }
+
+
+}

+ 22 - 0
src/PhpZip/Output/ZipOutputEmptyDirEntry.php

@@ -0,0 +1,22 @@
+<?php
+namespace PhpZip\Output;
+
+/**
+ * Zip output entry for empty dir.
+ *
+ * @author Ne-Lexa alexey@nelexa.ru
+ * @license MIT
+ */
+class ZipOutputEmptyDirEntry extends ZipOutputEntry
+{
+
+    /**
+     * Returns entry data.
+     *
+     * @return string
+     */
+    public function getEntryContent()
+    {
+        return '';
+    }
+}

+ 46 - 0
src/PhpZip/Output/ZipOutputEntry.php

@@ -0,0 +1,46 @@
+<?php
+namespace PhpZip\Output;
+
+use PhpZip\Model\ZipEntry;
+
+/**
+ * Zip output Entry
+ *
+ * @author Ne-Lexa alexey@nelexa.ru
+ * @license MIT
+ */
+abstract class ZipOutputEntry
+{
+    /**
+     * @var ZipEntry
+     */
+    private $entry;
+
+    /**
+     * @param ZipEntry $entry
+     */
+    public function __construct(ZipEntry $entry)
+    {
+        if ($entry === null) {
+            throw new \RuntimeException('entry is null');
+        }
+        $this->entry = $entry;
+    }
+
+    /**
+     * Returns zip entry
+     *
+     * @return ZipEntry
+     */
+    public function getEntry()
+    {
+        return $this->entry;
+    }
+
+    /**
+     * Returns entry data.
+     *
+     * @return string
+     */
+    abstract public function getEntryContent();
+}

+ 54 - 0
src/PhpZip/Output/ZipOutputStreamEntry.php

@@ -0,0 +1,54 @@
+<?php
+namespace PhpZip\Output;
+
+use PhpZip\Model\ZipEntry;
+use RuntimeException;
+
+/**
+ * Zip output entry for stream resource.
+ *
+ * @author Ne-Lexa alexey@nelexa.ru
+ * @license MIT
+ */
+class ZipOutputStreamEntry extends ZipOutputEntry
+{
+    /**
+     * @var resource
+     */
+    private $stream;
+
+    /**
+     * @param resource $stream
+     * @param ZipEntry $entry
+     */
+    public function __construct($stream, ZipEntry $entry)
+    {
+        parent::__construct($entry);
+        if (!is_resource($stream)) {
+            throw new RuntimeException('stream is not resource');
+        }
+        $this->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;
+        }
+    }
+}

+ 46 - 0
src/PhpZip/Output/ZipOutputStringEntry.php

@@ -0,0 +1,46 @@
+<?php
+namespace PhpZip\Output;
+
+use PhpZip\Exception\ZipException;
+use PhpZip\Model\ZipEntry;
+
+/**
+ * Zip output entry for string data.
+ *
+ * @author Ne-Lexa alexey@nelexa.ru
+ * @license MIT
+ */
+class ZipOutputStringEntry extends ZipOutputEntry
+{
+    /**
+     * Data content.
+     *
+     * @var string
+     */
+    private $data;
+
+    /**
+     * @param string $data
+     * @param ZipEntry $entry
+     * @throws ZipException If data empty.
+     */
+    public function __construct($data, ZipEntry $entry)
+    {
+        parent::__construct($entry);
+        $data = (string)$data;
+        if ($data === null) {
+            throw new ZipException("data is null");
+        }
+        $this->data = $data;
+    }
+
+    /**
+     * Returns entry data.
+     *
+     * @return string
+     */
+    public function getEntryContent()
+    {
+        return $this->data;
+    }
+}

+ 56 - 0
src/PhpZip/Output/ZipOutputZipFileEntry.php

@@ -0,0 +1,56 @@
+<?php
+namespace PhpZip\Output;
+
+use PhpZip\Exception\ZipException;
+use PhpZip\Model\ZipEntry;
+use PhpZip\ZipFile;
+
+/**
+ * Zip output entry for input zip file.
+ *
+ * @author Ne-Lexa alexey@nelexa.ru
+ * @license MIT
+ */
+class ZipOutputZipFileEntry extends ZipOutputEntry
+{
+    /**
+     * Input zip file.
+     *
+     * @var ZipFile
+     */
+    private $inputZipFile;
+
+    /**
+     * Input entry name.
+     *
+     * @var string
+     */
+    private $inputEntryName;
+
+    /**
+     * ZipOutputZipFileEntry constructor.
+     * @param ZipFile $zipFile
+     * @param ZipEntry $zipEntry
+     * @throws ZipException If input zip file is null.
+     */
+    public function __construct(ZipFile $zipFile, ZipEntry $zipEntry)
+    {
+        if ($zipFile === null) {
+            throw new ZipException('ZipFile is null');
+        }
+        parent::__construct(clone $zipEntry);
+
+        $this->inputZipFile = $zipFile;
+        $this->inputEntryName = $zipEntry->getName();
+    }
+
+    /**
+     * Returns entry data.
+     *
+     * @return string
+     */
+    public function getEntryContent()
+    {
+        return $this->inputZipFile->getEntryContent($this->inputEntryName);
+    }
+}

+ 32 - 0
src/PhpZip/Util/CryptoUtil.php

@@ -0,0 +1,32 @@
+<?php
+namespace PhpZip\Util;
+
+use PhpZip\Exception\ZipException;
+
+/**
+ * Crypto Utils
+ */
+class CryptoUtil
+{
+
+    /**
+     * Returns random bytes.
+     *
+     * @param int $length
+     * @return string
+     * @throws ZipException
+     */
+    public static final function randomBytes($length)
+    {
+        $length = (int)$length;
+        if (function_exists('random_bytes')) {
+            return random_bytes($length);
+        } elseif (function_exists('openssl_random_pseudo_bytes')) {
+            return openssl_random_pseudo_bytes($length);
+        } elseif (function_exists('mcrypt_create_iv')) {
+            return mcrypt_create_iv($length);
+        } else {
+            throw new ZipException('Extension openssl or mcrypt not loaded');
+        }
+    }
+}

+ 77 - 0
src/PhpZip/Util/DateTimeConverter.php

@@ -0,0 +1,77 @@
+<?php
+namespace PhpZip\Util;
+
+use PhpZip\Exception\ZipException;
+
+/**
+ * Convert unix timestamp values to DOS date/time values and vice versa.
+ *
+ * @author Ne-Lexa alexey@nelexa.ru
+ * @license MIT
+ */
+class DateTimeConverter
+{
+    /**
+     * Smallest supported DOS date/time value in a ZIP file,
+     * which is January 1st, 1980 AD 00:00:00 local time.
+     */
+    const MIN_DOS_TIME = 0x210000; // (1 << 21) | (1 << 16)
+
+    /**
+     * Largest supported DOS date/time value in a ZIP file,
+     * which is December 31st, 2107 AD 23:59:58 local time.
+     */
+    const MAX_DOS_TIME = 0xff9fbf7d; // ((2107 - 1980) << 25) | (12 << 21) | (31 << 16) | (23 << 11) | (59 << 5) | (58 >> 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);
+    }
+}

+ 222 - 0
src/PhpZip/Util/FilesUtil.php

@@ -0,0 +1,222 @@
+<?php
+namespace PhpZip\Util;
+
+use PhpZip\Util\Iterator\IgnoreFilesFilterIterator;
+use PhpZip\Util\Iterator\IgnoreFilesRecursiveFilterIterator;
+
+/**
+ * Files util.
+ *
+ * @author Ne-Lexa alexey@nelexa.ru
+ * @license MIT
+ */
+class FilesUtil
+{
+
+    /**
+     * Is empty directory
+     *
+     * @param string $dir Directory
+     * @return bool
+     */
+    public static function isEmptyDir($dir)
+    {
+        if (!is_readable($dir)) {
+            return false;
+        }
+        return count(scandir($dir)) === 2;
+    }
+
+    /**
+     * Remove recursive directory.
+     *
+     * @param string $dir Directory path.
+     */
+    public static function removeDir($dir)
+    {
+        $files = new \RecursiveIteratorIterator(
+            new \RecursiveDirectoryIterator($dir, \RecursiveDirectoryIterator::SKIP_DOTS),
+            \RecursiveIteratorIterator::CHILD_FIRST
+        );
+        foreach ($files as $fileInfo) {
+            $function = ($fileInfo->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";
+    }
+}

+ 60 - 0
src/PhpZip/Util/Iterator/IgnoreFilesFilterIterator.php

@@ -0,0 +1,60 @@
+<?php
+namespace PhpZip\Util\Iterator;
+
+use PhpZip\Util\StringUtil;
+
+/**
+ * Iterator for ignore files.
+ *
+ * @author Ne-Lexa alexey@nelexa.ru
+ * @license MIT
+ */
+class IgnoreFilesFilterIterator extends \FilterIterator
+{
+    /**
+     * Ignore list files
+     *
+     * @var array
+     */
+    private $ignoreFiles = ['..'];
+
+    /**
+     * @param \Iterator $iterator
+     * @param array $ignoreFiles
+     */
+    public function __construct(\Iterator $iterator, array $ignoreFiles)
+    {
+        parent::__construct($iterator);
+        $this->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;
+    }
+}

+ 69 - 0
src/PhpZip/Util/Iterator/IgnoreFilesRecursiveFilterIterator.php

@@ -0,0 +1,69 @@
+<?php
+namespace PhpZip\Util\Iterator;
+
+use PhpZip\Util\StringUtil;
+
+/**
+ * Recursive iterator for ignore files.
+ *
+ * @author Ne-Lexa alexey@nelexa.ru
+ * @license MIT
+ */
+class IgnoreFilesRecursiveFilterIterator extends \RecursiveFilterIterator
+{
+
+    /**
+     * Ignore list files
+     *
+     * @var array
+     */
+    private $ignoreFiles = ['..'];
+
+    /**
+     * @param \RecursiveIterator $iterator
+     * @param array $ignoreFiles
+     */
+    public function __construct(\RecursiveIterator $iterator, array $ignoreFiles)
+    {
+        parent::__construct($iterator);
+        $this->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);
+    }
+}

+ 44 - 0
src/PhpZip/Util/PackUtil.php

@@ -0,0 +1,44 @@
+<?php
+namespace PhpZip\Util;
+
+use PhpZip\Exception\ZipException;
+
+/**
+ * Pack util
+ *
+ * @author Ne-Lexa alexey@nelexa.ru
+ * @license MIT
+ */
+class PackUtil
+{
+
+    /**
+     * @param int|string $longValue
+     * @return string
+     */
+    public static function packLongLE($longValue)
+    {
+        // TODO test if (version_compare(PHP_VERSION, '5.6.3') >= 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);
+    }
+
+}

+ 30 - 0
src/PhpZip/Util/StringUtil.php

@@ -0,0 +1,30 @@
+<?php
+namespace PhpZip\Util;
+
+/**
+ * String Util
+ */
+class StringUtil
+{
+
+    /**
+     * @param string $haystack
+     * @param string $needle
+     * @return bool
+     */
+    public static function startsWith($haystack, $needle)
+    {
+        return $needle === "" || strrpos($haystack, $needle, -strlen($haystack)) !== false;
+    }
+
+    /**
+     * @param string $haystack
+     * @param string $needle
+     * @return bool
+     */
+    public static function endsWith($haystack, $needle)
+    {
+        return $needle === "" || (($temp = strlen($haystack) - strlen($needle)) >= 0
+            && strpos($haystack, $needle, $temp) !== false);
+    }
+}

+ 115 - 0
src/PhpZip/ZipConstants.php

@@ -0,0 +1,115 @@
+<?php
+namespace PhpZip;
+
+/**
+ * Constants for ZIP files.
+ *
+ * @author Ne-Lexa alexey@nelexa.ru
+ * @license MIT
+ */
+interface ZipConstants
+{
+    /** Local File Header signature. */
+    const LOCAL_FILE_HEADER_SIG = 0x04034B50;
+
+    /** Data Descriptor signature. */
+    const DATA_DESCRIPTOR_SIG = 0x08074B50;
+
+    /** Central File Header signature. */
+    const CENTRAL_FILE_HEADER_SIG = 0x02014B50;
+
+    /** Zip64 End Of Central Directory Record. */
+    const ZIP64_END_OF_CENTRAL_DIRECTORY_RECORD_SIG = 0x06064B50;
+
+    /** Zip64 End Of Central Directory Locator. */
+    const ZIP64_END_OF_CENTRAL_DIRECTORY_LOCATOR_SIG = 0x07064B50;
+
+    /** End Of Central Directory Record signature. */
+    const END_OF_CENTRAL_DIRECTORY_RECORD_SIG = 0x06054B50;
+
+    /**
+     * The minimum length of the Local File Header record.
+     *
+     * local file header signature      4
+     * version needed to extract        2
+     * general purpose bit flag         2
+     * compression method               2
+     * last mod file time               2
+     * last mod file date               2
+     * crc-32                           4
+     * compressed size                  4
+     * uncompressed size                4
+     * file name length                 2
+     * extra field length               2
+     */
+    const LOCAL_FILE_HEADER_MIN_LEN = 30;
+
+    /**
+     * The minimum length of the End Of Central Directory Record.
+     *
+     * end of central dir signature    4
+     * number of this disk             2
+     * number of the disk with the
+     * start of the central directory  2
+     * total number of entries in the
+     * central directory on this disk  2
+     * total number of entries in
+     * the central directory           2
+     * size of the central directory   4
+     * offset of start of central      *
+     * directory with respect to       *
+     * the starting disk number        4
+     * zipfile comment length          2
+     */
+    const END_OF_CENTRAL_DIRECTORY_RECORD_MIN_LEN = 22;
+
+    /**
+     * The length of the Zip64 End Of Central Directory Locator.
+     * zip64 end of central dir locator
+     * signature                       4
+     * number of the disk with the
+     * start of the zip64 end of
+     * central directory               4
+     * relative offset of the zip64
+     * end of central directory record 8
+     * total number of disks           4
+     */
+    const ZIP64_END_OF_CENTRAL_DIRECTORY_LOCATOR_LEN = 20;
+
+    /**
+     * The minimum length of the Zip64 End Of Central Directory Record.
+     *
+     * zip64 end of central dir
+     * signature                        4
+     * size of zip64 end of central
+     * directory record                 8
+     * version made by                  2
+     * version needed to extract        2
+     * number of this disk              4
+     * number of the disk with the
+     * start of the central directory   4
+     * total number of entries in the
+     * central directory on this disk   8
+     * total number of entries in
+     * the central directory            8
+     * size of the central directory    8
+     * offset of start of central
+     * directory with respect to
+     * the starting disk number         8
+     */
+    const ZIP64_END_OF_CENTRAL_DIRECTORY_RECORD_MIN_LEN = 56;
+
+    /**
+     * Local File Header signature      4
+     * Version Needed To Extract        2
+     * General Purpose Bit Flags        2
+     * Compression Method               2
+     * Last Mod File Time               2
+     * Last Mod File Date               2
+     * CRC-32                           4
+     * Compressed Size                  4
+     * Uncompressed Size                4
+     */
+    const LOCAL_FILE_HEADER_FILE_NAME_LENGTH_POS = 26;
+
+}

+ 908 - 0
src/PhpZip/ZipFile.php

@@ -0,0 +1,908 @@
+<?php
+namespace PhpZip;
+
+use PhpZip\Crypto\TraditionalPkwareEncryptionEngine;
+use PhpZip\Crypto\WinZipAesEngine;
+use PhpZip\Exception\Crc32Exception;
+use PhpZip\Exception\IllegalArgumentException;
+use PhpZip\Exception\ZipCryptoException;
+use PhpZip\Exception\ZipException;
+use PhpZip\Exception\ZipNotFoundEntry;
+use PhpZip\Exception\ZipUnsupportMethod;
+use PhpZip\Extra\WinZipAesEntryExtraField;
+use PhpZip\Mapper\OffsetPositionMapper;
+use PhpZip\Mapper\PositionMapper;
+use PhpZip\Model\ZipEntry;
+use PhpZip\Model\ZipInfo;
+use PhpZip\Util\PackUtil;
+
+/**
+ * This class is able to open the .ZIP file in read mode and extract files from it.
+ *
+ * Implemented support traditional PKWARE encryption and WinZip AES encryption.
+ * Implemented support ZIP64.
+ * Implemented support skip a preamble like the one found in self extracting archives.
+ *
+ * @see https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT .ZIP File Format Specification
+ * @author Ne-Lexa alexey@nelexa.ru
+ * @license MIT
+ */
+class ZipFile implements \Countable, \ArrayAccess, \Iterator, ZipConstants
+{
+    /**
+     * Input seekable stream resource.
+     *
+     * @var resource
+     */
+    private $inputStream;
+
+    /**
+     * The total number of bytes in the ZIP archive.
+     *
+     * @var int
+     */
+    private $length;
+
+    /**
+     * The charset to use for entry names and comments.
+     *
+     * @var string
+     */
+    private $charset;
+
+    /**
+     * The number of bytes in the preamble of this ZIP file.
+     *
+     * @var int
+     */
+    private $preamble;
+
+    /**
+     * The number of bytes in the postamble of this ZIP file.
+     *
+     * @var int
+     */
+    private $postamble;
+
+    /**
+     * Maps entry names to zip entries.
+     *
+     * @var ZipEntry[]
+     */
+    private $entries;
+
+    /**
+     * The file comment.
+     *
+     * @var string
+     */
+    private $comment;
+
+    /**
+     * Maps offsets specified in the ZIP file to real offsets in the file.
+     *
+     * @var PositionMapper
+     */
+    private $mapper;
+
+    /**
+     * Private ZipFile constructor.
+     *
+     * @see ZipFile::openFromFile()
+     * @see ZipFile::openFromString()
+     * @see ZipFile::openFromStream()
+     */
+    private function __construct()
+    {
+        $this->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);
+    }
+}

+ 1400 - 0
src/PhpZip/ZipOutputFile.php

@@ -0,0 +1,1400 @@
+<?php
+namespace PhpZip;
+
+use PhpZip\Crypto\TraditionalPkwareEncryptionEngine;
+use PhpZip\Crypto\WinZipAesEngine;
+use PhpZip\Exception\IllegalArgumentException;
+use PhpZip\Exception\ZipException;
+use PhpZip\Exception\ZipNotFoundEntry;
+use PhpZip\Extra\WinZipAesEntryExtraField;
+use PhpZip\Model\ZipEntry;
+use PhpZip\Output\ZipOutputEmptyDirEntry;
+use PhpZip\Output\ZipOutputEntry;
+use PhpZip\Output\ZipOutputStreamEntry;
+use PhpZip\Output\ZipOutputStringEntry;
+use PhpZip\Output\ZipOutputZipFileEntry;
+use PhpZip\Util\FilesUtil;
+use PhpZip\Util\PackUtil;
+
+/**
+ * This class is able to create or update the .ZIP file in write mode.
+ *
+ * Implemented support traditional PKWARE encryption and WinZip AES encryption.
+ * Implemented support ZIP64.
+ * Implemented support skip a preamble like the one found in self extracting archives.
+ *
+ * @see https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT .ZIP File Format Specification
+ * @author Ne-Lexa alexey@nelexa.ru
+ * @license MIT
+ */
+class ZipOutputFile implements \Countable, \ArrayAccess, \Iterator, ZipConstants
+{
+    /**
+     * Compression level for fastest compression.
+     */
+    const LEVEL_BEST_SPEED = 1;
+
+    /**
+     * Compression level for best compression.
+     */
+    const LEVEL_BEST_COMPRESSION = 9;
+
+    /**
+     * Default compression level.
+     */
+    const LEVEL_DEFAULT_COMPRESSION = -1;
+
+    /**
+     * Allow compression methods.
+     *
+     * @var array
+     */
+    private static $allowCompressionMethods = [
+        ZipEntry::METHOD_STORED,
+        ZipEntry::METHOD_DEFLATED,
+        ZipEntry::METHOD_BZIP2
+    ];
+
+    /**
+     * Default mime types.
+     *
+     * @var array
+     */
+    private static $defaultMimeTypes = [
+        'zip' => '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);
+    }
+}

+ 1049 - 0
tests/PhpZip/ZipTest.php

@@ -0,0 +1,1049 @@
+<?php
+namespace PhpZip;
+
+use PhpZip\Exception\ZipAuthenticationException;
+use PhpZip\Model\ZipEntry;
+use PhpZip\Util\CryptoUtil;
+use PhpZip\Util\FilesUtil;
+
+/**
+ * ZipFile and ZipOutputFile test
+ */
+class ZipTest extends ZipTestCase
+{
+    /**
+     * @var string
+     */
+    private $outputFilename;
+
+    /**
+     * Before test
+     */
+    protected function setUp()
+    {
+        parent::setUp();
+
+        $this->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('<?php', $decryptContent);
+        }
+
+        // change encryption method
+        $outputZip = ZipOutputFile::openFromZipFile($zipFile);
+        $outputZip->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('<?php', $decryptContent);
+        }
+
+        // clear password
+        $outputZip = ZipOutputFile::openFromZipFile($zipFile);
+        $outputZip->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();
+    }
+
+}

+ 45 - 0
tests/PhpZip/ZipTestCase.php

@@ -0,0 +1,45 @@
+<?php
+namespace PhpZip;
+
+/**
+ * PHPUnit test case and helper methods.
+ */
+class ZipTestCase extends \PHPUnit_Framework_TestCase
+{
+    /**
+     * Assert correct zip archive.
+     *
+     * @param $filename
+     */
+    public static function assertCorrectZipArchive($filename)
+    {
+        if (DIRECTORY_SEPARATOR !== '\\' && `which zip`) {
+            exec("zip -T " . escapeshellarg($filename), $output, $returnCode);
+
+            $output = implode(PHP_EOL, $output);
+
+            self::assertEquals($returnCode, 0);
+            self::assertNotContains('zip error', $output);
+            self::assertContains(' OK', $output);
+        }
+    }
+
+    /**
+     * Assert correct empty zip archive.
+     *
+     * @param $filename
+     */
+    public static function assertCorrectEmptyZip($filename)
+    {
+        if (DIRECTORY_SEPARATOR !== '\\' && `which zipinfo`) {
+            exec("zipinfo " . escapeshellarg($filename), $output, $returnCode);
+
+            $output = implode(PHP_EOL, $output);
+
+            self::assertContains('Empty zipfile', $output);
+        }
+        $actualEmptyZipData = pack('VVVVVv', ZipConstants::END_OF_CENTRAL_DIRECTORY_RECORD_SIG, 0, 0, 0, 0, 0);
+        self::assertEquals(file_get_contents($filename), $actualEmptyZipData);
+    }
+
+}

+ 0 - 244
tests/TestZipFile.php

@@ -1,244 +0,0 @@
-<?php
-
-class TestZipFile extends PHPUnit_Framework_TestCase
-{
-
-    public function testCreate()
-    {
-        $output = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'test-create.zip';
-        $extractOutputDir = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'test-create';
-
-        $listFilename = 'files.txt';
-        $listFileContent = implode(PHP_EOL, glob('*'));
-        $dirName = 'src/';
-        $archiveComment = 'Archive comment - 😀';
-        $commentIndex0 = basename(__FILE__);
-
-        $zip = new \Nelexa\Zip\ZipFile();
-        $zip->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;
-    }
-}

BIN
tests/res/file.apk


+ 0 - 28
tests/res/private.pem

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

+ 0 - 21
tests/res/public.pem

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