Parcourir la source

Merge ZipFile and ZipOutputFile, optimization update archive.

wapplay-home-linux il y a 9 ans
Parent
commit
f802861d86

+ 46 - 13
README.md

@@ -1,12 +1,20 @@
-`PhpZip` Version 2
-================
-`PhpZip` - is to create, update, opening and unpacking ZIP archives in pure PHP.
+`PhpZip` (ver 3.0.+)
+====================
+`PhpZip` - php library for manipulating zip archives.
 
-The library supports `ZIP64`, `zipalign`, `Traditional PKWARE Encryption` and `WinZIP AES Encryption`.
-
-ZIP64 extensions are automatically and transparently activated when reading or writing ZIP files of more than 4 GB size.
-
-The library does not require extension `php-zip` and class `ZipArchive`.
+Features:
+---------
+- Opening and unzipping zip files.
+- Create zip files.
+- Update zip files.
+- Pure php (not require extension `php-zip` and class `\ZipArchive`).
+- Output the modified archive as a string or output to the browser without saving the result to disk.
+- Support archive comment and entries comments.
+- Get info of zip entries.
+- Support zip password for PHP < 5.6.0 (`\ZipArchive` required this version), include update and remove password.
+- Support encryption method `Traditional PKWARE Encryption (ZipCrypto)` and `WinZIP AES Encryption`.
+- Support `ZIP64` (size > 4 GiB or files > 65535 in a .ZIP archive).
+- Support archive alignment functional [`zipalign`](https://developer.android.com/studio/command-line/zipalign.html).
 
 Requirements
 ------------
@@ -17,19 +25,44 @@ Requirements
 
 Installation
 ------------
-`composer require nelexa/zip`
+`composer require nelexa/zip:^3.0`
+
+Samples
+-------
+```php
+// create archive
+$zipFile = new \PhpZip\ZipFile();
+$zipFile->addFromString("zip/entry/filename", "Is file content")
+        ->addFile("/path/to/file", "data/tofile")
+        ->addDir(__DIR__, "to/path/")
+        ->saveAsFile($outputFilename)
+        ->close();
+        
+// open archive, extract, add files, set password and output to browser.
+$zipFile->openFile($outputFilename)
+        ->extractTo($outputDirExtract)
+        ->deleteFromRegex('~^\.~') // delete all hidden (Unix) files
+        ->addFromString('dir/file.txt', 'Test file')
+        ->withNewPassword('password')
+        ->outputAsAttachment('library.jar');
+```
+Other examples can be found in the `tests/` folder
 
 Documentation
 -------------
-#### Class `\PhpZip\ZipFile` (open, extract, info)
+
+
+
 Open zip archive from file.
 ```php
-$zipFile = \PhpZip\ZipFile::openFromFile($filename);
+$zipFile = new \PhpZip\ZipFile();
+$zipFile->openFile($filename);
 ```
 Open zip archive from data string.
 ```php
-$data = file_get_contents($filename);
-$zipFile = \PhpZip\ZipFile::openFromString($data);
+$data = file_get_contents($urlOrFile);
+$zipFile = new \PhpZip\ZipFile();
+$zipFile->openFromString($filename);
 ```
 Open zip archive from stream resource.
 ```php

+ 10 - 3
composer.json

@@ -4,6 +4,7 @@
   "type": "library",
   "keywords": [
     "zip",
+    "unzip",
     "archive",
     "extract",
     "winzip",
@@ -23,16 +24,22 @@
   "minimum-stability": "stable",
   "require": {
     "php-64bit": "^5.4 || ^7.0",
-    "ext-mbstring": "*"
+    "ext-mbstring": "*",
+    "nelexa/buffer": "^1.1"
   },
   "autoload": {
     "psr-4": {
-      "": "src/"
+      "PhpZip\\": "src/PhpZip"
     }
   },
   "autoload-dev": {
     "psr-4": {
-      "": "tests/"
+      "PhpZip\\": "tests/PhpZip"
     }
+  },
+  "suggest": {
+    "ext-openssl": "Needed to support encrypt zip entries or use ext-mcrypt",
+    "ext-mcrypt": "Needed to support encrypt zip entries or use ext-openssl",
+    "ext-bz2": "Needed to support BZIP2 compression"
   }
 }

+ 24 - 0
src/PhpZip/Crypto/CryptoEngine.php

@@ -0,0 +1,24 @@
+<?php
+namespace PhpZip\Crypto;
+
+use PhpZip\Exception\ZipAuthenticationException;
+
+interface CryptoEngine
+{
+    /**
+     * Decryption string.
+     *
+     * @param string $encryptionContent
+     * @return string
+     * @throws ZipAuthenticationException
+     */
+    public function decrypt($encryptionContent);
+
+    /**
+     * Encryption string.
+     *
+     * @param string $content
+     * @return string
+     */
+    public function encrypt($content);
+}

+ 10 - 6
src/PhpZip/Crypto/TraditionalPkwareEncryptionEngine.php

@@ -2,6 +2,7 @@
 namespace PhpZip\Crypto;
 
 use PhpZip\Exception\ZipAuthenticationException;
+use PhpZip\Exception\ZipCryptoException;
 use PhpZip\Model\ZipEntry;
 use PhpZip\Util\CryptoUtil;
 
@@ -12,7 +13,7 @@ use PhpZip\Util\CryptoUtil;
  * @author Ne-Lexa alexey@nelexa.ru
  * @license MIT
  */
-class TraditionalPkwareEncryptionEngine
+class TraditionalPkwareEncryptionEngine implements CryptoEngine
 {
     /**
      * Encryption header size
@@ -154,7 +155,7 @@ class TraditionalPkwareEncryptionEngine
 
         if ($this->entry->getGeneralPurposeBitFlag(ZipEntry::GPBF_DATA_DESCRIPTOR)) {
             // compare against the file type from extended local headers
-            $checkByte = ($this->entry->getRawTime() >> 8) & 0xff;
+            $checkByte = ($this->entry->getTime() >> 8) & 0xff;
         } else {
             // compare against the CRC otherwise
             $checkByte = ($this->entry->getCrc() >> 24) & 0xff;
@@ -187,11 +188,13 @@ class TraditionalPkwareEncryptionEngine
      * Encryption data
      *
      * @param string $data
-     * @param int $crc
      * @return string
      */
-    public function encrypt($data, $crc)
+    public function encrypt($data)
     {
+        $crc = ($this->entry->isDataDescriptorRequired() ?
+            ($this->entry->getTime() & 0x0000ffff) << 16 :
+            $this->entry->getCrc());
         $headerBytes = CryptoUtil::randomBytes(self::STD_DEC_HDR_SIZE);
 
         // Initialize again since the generated bytes were encrypted.
@@ -206,11 +209,12 @@ class TraditionalPkwareEncryptionEngine
     /**
      * @param string $content
      * @return string
+     * @throws ZipCryptoException
      */
     private function encryptData($content)
     {
         if ($content === null) {
-            throw new \RuntimeException();
+            throw new ZipCryptoException('content is null');
         }
         $buff = '';
         foreach (unpack('C*', $content) as $val) {
@@ -223,7 +227,7 @@ class TraditionalPkwareEncryptionEngine
      * @param int $byte
      * @return int
      */
-    protected function encryptByte($byte)
+    private function encryptByte($byte)
     {
         $tempVal = $byte ^ $this->decryptByte() & 0xff;
         $this->updateKeys($byte);

+ 29 - 21
src/PhpZip/Crypto/WinZipAesEngine.php

@@ -1,6 +1,7 @@
 <?php
 namespace PhpZip\Crypto;
 
+use PhpZip\Exception\RuntimeException;
 use PhpZip\Exception\ZipAuthenticationException;
 use PhpZip\Exception\ZipCryptoException;
 use PhpZip\Extra\WinZipAesEntryExtraField;
@@ -13,7 +14,7 @@ use PhpZip\Util\CryptoUtil;
  * @author Ne-Lexa alexey@nelexa.ru
  * @license MIT
  */
-class WinZipAesEngine
+class WinZipAesEngine implements CryptoEngine
 {
     /**
      * The block size of the Advanced Encryption Specification (AES) Algorithm
@@ -42,12 +43,12 @@ class WinZipAesEngine
     /**
      * Decrypt from stream resource.
      *
-     * @param resource $stream Input stream resource
+     * @param string $content Input stream buffer
      * @return string
      * @throws ZipAuthenticationException
      * @throws ZipCryptoException
      */
-    public function decrypt($stream)
+    public function decrypt($content)
     {
         /**
          * @var WinZipAesEntryExtraField $field
@@ -57,20 +58,20 @@ class WinZipAesEngine
             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);
+        $pos = $keyStrengthBytes / 2;
+        $salt = substr($content, 0, $pos);
+        $passwordVerifier = substr($content, $pos, self::PWD_VERIFIER_BITS / 8);
+        $pos += self::PWD_VERIFIER_BITS / 8;
 
         $sha1Size = 20;
 
         // Init start, end and size of encrypted data.
-        $endPos = $pos + $this->entry->getCompressedSize();
-        $start = ftell($stream);
+        $start = $pos;
+        $endPos = strlen($content);
         $footerSize = $sha1Size / 2;
         $end = $endPos - $footerSize;
         $size = $end - $start;
@@ -80,9 +81,8 @@ class WinZipAesEngine
         }
 
         // Load authentication code.
-        fseek($stream, $end, SEEK_SET);
-        $authenticationCode = fread($stream, $footerSize);
-        if (ftell($stream) !== $endPos) {
+        $authenticationCode = substr($content, $end, $footerSize);
+        if ($end + $footerSize !== $endPos) {
             // This should never happen unless someone is writing to the
             // end of the file concurrently!
             throw new ZipCryptoException("Expected end of file after WinZip AES authentication code!");
@@ -95,27 +95,33 @@ class WinZipAesEngine
         // WinZip 99-character limit
         // @see https://sourceforge.net/p/p7zip/discussion/383044/thread/c859a2f0/
         $password = substr($password, 0, 99);
+        $ctrIvSize = self::AES_BLOCK_SIZE_BITS / 8;
+        $iv = str_repeat(chr(0), $ctrIvSize);
         do {
             // Here comes the strange part about WinZip AES encryption:
             // Its unorthodox use of the Password-Based Key Derivation
             // Function 2 (PBKDF2) of PKCS #5 V2.0 alias RFC 2898.
             // Yes, the password verifier is only a 16 bit value.
             // So we must use the MAC for password verification, too.
-            $keyParam = hash_pbkdf2("sha1", $password, $salt, self::ITERATION_COUNT, (2 * $keyStrengthBits + self::PWD_VERIFIER_BITS) / 8, true);
-            $ctrIvSize = self::AES_BLOCK_SIZE_BITS / 8;
-            $iv = str_repeat(chr(0), $ctrIvSize);
-
+            $keyParam = hash_pbkdf2(
+                "sha1",
+                $password,
+                $salt,
+                self::ITERATION_COUNT,
+                (2 * $keyStrengthBits + self::PWD_VERIFIER_BITS) / 8,
+                true
+            );
             $key = substr($keyParam, 0, $keyStrengthBytes);
-
             $sha1MacParam = substr($keyParam, $keyStrengthBytes, $keyStrengthBytes);
             // Verify password.
         } while (!$passwordVerifier === substr($keyParam, 2 * $keyStrengthBytes));
 
-        $content = stream_get_contents($stream, $size, $start);
+        $content = substr($content, $start, $size);
         $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)");
+            throw new ZipAuthenticationException($this->entry->getName() .
+                " (authenticated WinZip AES entry content has been tampered with)");
         }
 
         return self::aesCtrSegmentIntegerCounter(false, $content, $key, $iv);
@@ -161,6 +167,7 @@ class WinZipAesEngine
      * @param string $key Aes key
      * @param string $iv Aes IV
      * @return string Encrypted data
+     * @throws RuntimeException
      */
     private static function encryptCtr($data, $key, $iv)
     {
@@ -170,7 +177,7 @@ class WinZipAesEngine
         } elseif (extension_loaded("mcrypt")) {
             return mcrypt_encrypt(MCRYPT_RIJNDAEL_128, $key, $data, "ctr", $iv);
         } else {
-            throw new \RuntimeException('Extension openssl or mcrypt not loaded');
+            throw new RuntimeException('Extension openssl or mcrypt not loaded');
         }
     }
 
@@ -181,6 +188,7 @@ class WinZipAesEngine
      * @param string $key Aes key
      * @param string $iv Aes IV
      * @return string Raw data
+     * @throws RuntimeException
      */
     private static function decryptCtr($data, $key, $iv)
     {
@@ -190,7 +198,7 @@ class WinZipAesEngine
         } elseif (extension_loaded("mcrypt")) {
             return mcrypt_decrypt(MCRYPT_RIJNDAEL_128, $key, $data, "ctr", $iv);
         } else {
-            throw new \RuntimeException('Extension openssl or mcrypt not loaded');
+            throw new RuntimeException('Extension openssl or mcrypt not loaded');
         }
     }
 

+ 1 - 1
src/PhpZip/Exception/IllegalArgumentException.php → src/PhpZip/Exception/InvalidArgumentException.php

@@ -8,7 +8,7 @@ namespace PhpZip\Exception;
  * @author Ne-Lexa alexey@nelexa.ru
  * @license MIT
  */
-class IllegalArgumentException extends ZipException
+class InvalidArgumentException extends ZipException
 {
 
 }

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

@@ -0,0 +1,13 @@
+<?php
+namespace PhpZip\Exception;
+
+/**
+ * Runtime exception.
+ *
+ * @author Ne-Lexa alexey@nelexa.ru
+ * @license MIT
+ */
+class RuntimeException extends ZipException
+{
+
+}

+ 1 - 1
src/PhpZip/Extra/ExtraField.php

@@ -80,7 +80,7 @@ abstract class ExtraField implements ExtraFieldHeader
         if (0x0000 > $size || $size > 0xffff) {
             throw new ZipException('size data block out of range.');
         }
-        $fp = fopen('php://temp', 'r+b');
+        $fp = fopen('php://memory', 'r+b');
         if (0 === $size) return $fp;
         $this->writeTo($fp, 0);
         rewind($fp);

+ 12 - 25
src/PhpZip/Extra/ExtraFields.php

@@ -1,7 +1,6 @@
 <?php
 namespace PhpZip\Extra;
 
-
 use PhpZip\Exception\ZipException;
 
 /**
@@ -118,8 +117,17 @@ class ExtraFields
         }
         if (0 === $size) return '';
 
-        $fp = fopen('php://temp', 'r+b');
-        $this->writeTo($fp, 0);
+        $fp = fopen('php://memory', 'r+b');
+        $offset = 0;
+        /**
+         * @var ExtraField $ef
+         */
+        foreach ($this->extra as $ef) {
+            fwrite($fp, pack('vv', $ef::getHeaderId(), $ef->getDataSize()));
+            $offset += 4;
+            fwrite($fp, $ef->writeTo($fp, $offset));
+            $offset += $ef->getDataSize();
+        }
         rewind($fp);
         $content = stream_get_contents($fp);
         fclose($fp);
@@ -148,27 +156,6 @@ class ExtraFields
         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.
@@ -187,7 +174,7 @@ class ExtraFields
         if (null !== $handle && 0 < $size) {
             $end = $off + $size;
             while ($off < $end) {
-                fseek($handle, $off, SEEK_SET);
+                fseek($handle, $off);
                 $unpack = unpack('vheaderId/vdataSize', fread($handle, 4));
                 $off += 4;
                 $extraField = ExtraField::create($unpack['headerId']);

+ 466 - 0
src/PhpZip/Model/CentralDirectory.php

@@ -0,0 +1,466 @@
+<?php
+namespace PhpZip\Model;
+
+use PhpZip\Exception\InvalidArgumentException;
+use PhpZip\Exception\ZipException;
+use PhpZip\Exception\ZipNotFoundEntry;
+use PhpZip\Model\Entry\ZipNewStringEntry;
+use PhpZip\Model\Entry\ZipReadEntry;
+use PhpZip\ZipFile;
+
+/**
+ * Read Central Directory
+ *
+ * @author Ne-Lexa alexey@nelexa.ru
+ * @license MIT
+ */
+class CentralDirectory
+{
+    /** Central File Header signature. */
+    const CENTRAL_FILE_HEADER_SIG = 0x02014B50;
+    /**
+     * @var EndOfCentralDirectory End of Central Directory
+     */
+    private $endOfCentralDirectory;
+    /**
+     * @var ZipEntry[] Maps entry names to zip entries.
+     */
+    private $entries = [];
+    /**
+     * @var ZipEntry[] New and modified entries
+     */
+    private $modifiedEntries = [];
+    /**
+     * @var int Default compression level for the methods DEFLATED and BZIP2.
+     */
+    private $compressionLevel = ZipFile::LEVEL_DEFAULT_COMPRESSION;
+    /**
+     * @var int|null ZipAlign setting
+     */
+    private $zipAlign;
+    /**
+     * @var string New password
+     */
+    private $password;
+    /**
+     * @var int
+     */
+    private $encryptionMethod;
+    /**
+     * @var bool
+     */
+    private $clearPassword;
+
+    public function __construct()
+    {
+        $this->endOfCentralDirectory = new EndOfCentralDirectory();
+    }
+
+    /**
+     * Reads the central directory from the given seekable byte channel
+     * and populates the internal tables with ZipEntry instances.
+     *
+     * The ZipEntry's will know all data that can be obtained from the
+     * central directory alone, but not the data that requires the local
+     * file header or additional data to be read.
+     *
+     * @param resource $inputStream
+     * @throws ZipException
+     */
+    public function mountCentralDirectory($inputStream)
+    {
+        $this->modifiedEntries = [];
+        $this->checkZipFileSignature($inputStream);
+        $this->endOfCentralDirectory->findCentralDirectory($inputStream);
+
+        $numEntries = $this->endOfCentralDirectory->getCentralDirectoryEntriesSize();
+        $entries = [];
+        for (; $numEntries > 0; $numEntries--) {
+            $entry = new ZipReadEntry($inputStream);
+            $entry->setCentralDirectory($this);
+            // Re-load virtual offset after ZIP64 Extended Information
+            // Extra Field may have been parsed, map it to the real
+            // offset and conditionally update the preamble size from it.
+            $lfhOff = $this->endOfCentralDirectory->getMapper()->map($entry->getOffset());
+            if ($lfhOff < $this->endOfCentralDirectory->getPreamble()) {
+                $this->endOfCentralDirectory->setPreamble($lfhOff);
+            }
+            $entries[$entry->getName()] = $entry;
+        }
+
+        if (0 !== $numEntries % 0x10000) {
+            throw new ZipException("Expected " . abs($numEntries) .
+                ($numEntries > 0 ? " more" : " less") .
+                " entries in the Central Directory!");
+        }
+        $this->entries = $entries;
+
+        if ($this->endOfCentralDirectory->getPreamble() + $this->endOfCentralDirectory->getPostamble() >= fstat($inputStream)['size']) {
+            assert(0 === $numEntries);
+            $this->checkZipFileSignature($inputStream);
+        }
+    }
+
+    /**
+     * Check zip file signature
+     *
+     * @param resource $inputStream
+     * @throws ZipException if this not .ZIP file.
+     */
+    private function checkZipFileSignature($inputStream)
+    {
+        rewind($inputStream);
+        // Constraint: A ZIP file must start with a Local File Header
+        // or a (ZIP64) End Of Central Directory Record if it's empty.
+        $signature = unpack('V', fread($inputStream, 4))[1];
+        if (
+            ZipEntry::LOCAL_FILE_HEADER_SIG !== $signature
+            && EndOfCentralDirectory::ZIP64_END_OF_CENTRAL_DIRECTORY_RECORD_SIG !== $signature
+            && EndOfCentralDirectory::END_OF_CENTRAL_DIRECTORY_RECORD_SIG !== $signature
+        ) {
+            throw new ZipException("Expected Local File Header or (ZIP64) End Of Central Directory Record! Signature: " . $signature);
+        }
+    }
+
+    /**
+     * Set compression method for new or rewrites entries.
+     * @param int $compressionLevel
+     * @throws InvalidArgumentException
+     * @see ZipFile::LEVEL_DEFAULT_COMPRESSION
+     * @see ZipFile::LEVEL_BEST_SPEED
+     * @see ZipFile::LEVEL_BEST_COMPRESSION
+     */
+    public function setCompressionLevel($compressionLevel = ZipFile::LEVEL_DEFAULT_COMPRESSION)
+    {
+        if ($compressionLevel < ZipFile::LEVEL_DEFAULT_COMPRESSION ||
+            $compressionLevel > ZipFile::LEVEL_BEST_COMPRESSION
+        ) {
+            throw new InvalidArgumentException('Invalid compression level. Minimum level ' .
+                ZipFile::LEVEL_DEFAULT_COMPRESSION . '. Maximum level ' . ZipFile::LEVEL_BEST_COMPRESSION);
+        }
+        $this->compressionLevel = $compressionLevel;
+    }
+
+    /**
+     * @return ZipEntry[]
+     */
+    public function &getEntries()
+    {
+        return $this->entries;
+    }
+
+    /**
+     * @param string $entryName
+     * @return ZipEntry
+     * @throws ZipNotFoundEntry
+     */
+    public function getEntry($entryName)
+    {
+        if (!isset($this->entries[$entryName])) {
+            throw new ZipNotFoundEntry('Zip entry ' . $entryName . ' not found');
+        }
+        return $this->entries[$entryName];
+    }
+
+    /**
+     * @return EndOfCentralDirectory
+     */
+    public function getEndOfCentralDirectory()
+    {
+        return $this->endOfCentralDirectory;
+    }
+
+    public function getArchiveComment()
+    {
+        return null === $this->endOfCentralDirectory->getComment() ?
+            '' :
+            $this->endOfCentralDirectory->getComment();
+    }
+
+    /**
+     * Set entry comment
+     * @param string $entryName
+     * @param string|null $comment
+     * @throws ZipNotFoundEntry
+     */
+    public function setEntryComment($entryName, $comment)
+    {
+        if (isset($this->modifiedEntries[$entryName])) {
+            $this->modifiedEntries[$entryName]->setComment($comment);
+        } elseif (isset($this->entries[$entryName])) {
+            $entry = clone $this->entries[$entryName];
+            $entry->setComment($comment);
+            $this->putInModified($entryName, $entry);
+        } else {
+            throw new ZipNotFoundEntry("Not found entry " . $entryName);
+        }
+    }
+
+    /**
+     * @param string|null $password
+     * @param int|null $encryptionMethod
+     */
+    public function setNewPassword($password, $encryptionMethod = null)
+    {
+        $this->password = $password;
+        $this->encryptionMethod = $encryptionMethod;
+        $this->clearPassword = $password === null;
+    }
+
+    /**
+     * @return int|null
+     */
+    public function getZipAlign()
+    {
+        return $this->zipAlign;
+    }
+
+    /**
+     * @param int|null $zipAlign
+     */
+    public function setZipAlign($zipAlign = null)
+    {
+        if ($zipAlign === null) {
+            $this->zipAlign = null;
+            return;
+        }
+        $this->zipAlign = (int)$zipAlign;
+    }
+
+    /**
+     * Put modification or new entries.
+     *
+     * @param $entryName
+     * @param ZipEntry $entry
+     */
+    public function putInModified($entryName, ZipEntry $entry)
+    {
+        $this->modifiedEntries[$entryName] = $entry;
+    }
+
+    /**
+     * @param string $entryName
+     * @throws ZipNotFoundEntry
+     */
+    public function deleteEntry($entryName)
+    {
+        if (isset($this->entries[$entryName])) {
+            $this->modifiedEntries[$entryName] = null;
+        } elseif (isset($this->modifiedEntries[$entryName])) {
+            unset($this->modifiedEntries[$entryName]);
+        } else {
+            throw new ZipNotFoundEntry("Not found entry " . $entryName);
+        }
+    }
+
+    /**
+     * @param string $regexPattern
+     * @return bool
+     */
+    public function deleteEntriesFromRegex($regexPattern)
+    {
+        $count = 0;
+        foreach ($this->modifiedEntries as $entryName => &$entry) {
+            if (preg_match($regexPattern, $entryName)) {
+                unset($entry);
+                $count++;
+            }
+        }
+        foreach ($this->entries as $entryName => $entry) {
+            if (preg_match($regexPattern, $entryName)) {
+                $this->modifiedEntries[$entryName] = null;
+                $count++;
+            }
+        }
+        return $count > 0;
+    }
+
+    /**
+     * @param string $oldName
+     * @param string $newName
+     * @throws InvalidArgumentException
+     * @throws ZipNotFoundEntry
+     */
+    public function rename($oldName, $newName)
+    {
+        $oldName = (string)$oldName;
+        $newName = (string)$newName;
+
+        if (isset($this->entries[$newName]) || isset($this->modifiedEntries[$newName])) {
+            throw new InvalidArgumentException("New entry name " . $newName . ' is exists.');
+        }
+
+        if (isset($this->modifiedEntries[$oldName]) || isset($this->entries[$oldName])) {
+            $newEntry = clone (isset($this->modifiedEntries[$oldName]) ?
+                $this->modifiedEntries[$oldName] :
+                $this->entries[$oldName]);
+            $newEntry->setName($newName);
+
+            $this->modifiedEntries[$oldName] = null;
+            $this->modifiedEntries[$newName] = $newEntry;
+            return;
+        }
+        throw new ZipNotFoundEntry("Not found entry " . $oldName);
+    }
+
+    /**
+     * Delete all entries.
+     */
+    public function deleteAll()
+    {
+        $this->modifiedEntries = [];
+        foreach ($this->entries as $entry) {
+            $this->modifiedEntries[$entry->getName()] = null;
+        }
+    }
+
+    /**
+     * @param resource $outputStream
+     */
+    public function writeArchive($outputStream)
+    {
+        /**
+         * @var ZipEntry[] $memoryEntriesResult
+         */
+        $memoryEntriesResult = [];
+        foreach ($this->entries as $entryName => $entry) {
+            if (isset($this->modifiedEntries[$entryName])) continue;
+
+            if (
+                ($this->password !== null || $this->clearPassword) &&
+                $entry->isEncrypted() &&
+                $entry->getPassword() !== null &&
+                (
+                    $entry->getPassword() !== $this->password ||
+                    $entry->getEncryptionMethod() !== $this->encryptionMethod
+                )
+            ) {
+                $prototypeEntry = new ZipNewStringEntry($entry->getEntryContent());
+                $prototypeEntry->setName($entry->getName());
+                $prototypeEntry->setMethod($entry->getMethod());
+                $prototypeEntry->setTime($entry->getTime());
+                $prototypeEntry->setExternalAttributes($entry->getExternalAttributes());
+                $prototypeEntry->setExtra($entry->getExtra());
+                $prototypeEntry->setPassword($this->password, $this->encryptionMethod);
+                if($this->clearPassword){
+                    $prototypeEntry->clearEncryption();
+                }
+            } else {
+                $prototypeEntry = clone $entry;
+            }
+            $memoryEntriesResult[$entryName] = $prototypeEntry;
+        }
+
+        foreach ($this->modifiedEntries as $entryName => $outputEntry) {
+            if (null === $outputEntry) { // remove marked entry
+                unset($memoryEntriesResult[$entryName]);
+            } else {
+                if ($this->password !== null) {
+                    $outputEntry->setPassword($this->password, $this->encryptionMethod);
+                }
+                $memoryEntriesResult[$entryName] = $outputEntry;
+            }
+        }
+
+        foreach ($memoryEntriesResult as $key => $outputEntry) {
+            $outputEntry->setCentralDirectory($this);
+            $outputEntry->writeEntry($outputStream);
+        }
+        $centralDirectoryOffset = ftell($outputStream);
+        foreach ($memoryEntriesResult as $key => $outputEntry) {
+            if (!$this->writeCentralFileHeader($outputStream, $outputEntry)) {
+                unset($memoryEntriesResult[$key]);
+            }
+        }
+        $centralDirectoryEntries = sizeof($memoryEntriesResult);
+        $this->getEndOfCentralDirectory()->writeEndOfCentralDirectory(
+            $outputStream,
+            $centralDirectoryEntries,
+            $centralDirectoryOffset
+        );
+    }
+
+    /**
+     * Writes a Central File Header record.
+     *
+     * @param resource $outputStream
+     * @param ZipEntry $entry
+     * @return bool false if and only if the record has been skipped,
+     *         i.e. not written for some other reason than an I/O error.
+     */
+    private function writeCentralFileHeader($outputStream, ZipEntry $entry)
+    {
+        $compressedSize = $entry->getCompressedSize();
+        $size = $entry->getSize();
+        // This test MUST NOT include the CRC-32 because VV_AE_2 sets it to
+        // UNKNOWN!
+        if (ZipEntry::UNKNOWN === ($compressedSize | $size)) {
+            return false;
+        }
+        $extra = $entry->getExtra();
+        $extraSize = strlen($extra);
+
+        $commentLength = strlen($entry->getComment());
+        fwrite(
+            $outputStream,
+            pack(
+                'VvvvvVVVVvvvvvVV',
+                // central file header signature   4 bytes  (0x02014b50)
+                self::CENTRAL_FILE_HEADER_SIG,
+                // version made by                 2 bytes
+                ($entry->getPlatform() << 8) | 63,
+                // version needed to extract       2 bytes
+                $entry->getVersionNeededToExtract(),
+                // general purpose bit flag        2 bytes
+                $entry->getGeneralPurposeBitFlags(),
+                // compression method              2 bytes
+                $entry->getMethod(),
+                // last mod file datetime          4 bytes
+                $entry->getTime(),
+                // crc-32                          4 bytes
+                $entry->getCrc(),
+                // compressed size                 4 bytes
+                $entry->getCompressedSize(),
+                // uncompressed size               4 bytes
+                $entry->getSize(),
+                // file name length                2 bytes
+                strlen($entry->getName()),
+                // extra field length              2 bytes
+                $extraSize,
+                // file comment length             2 bytes
+                $commentLength,
+                // disk number start               2 bytes
+                0,
+                // internal file attributes        2 bytes
+                0,
+                // external file attributes        4 bytes
+                $entry->getExternalAttributes(),
+                // relative offset of local header 4 bytes
+                $entry->getOffset()
+            )
+        );
+        // file name (variable size)
+        fwrite($outputStream, $entry->getName());
+        if (0 < $extraSize) {
+            // extra field (variable size)
+            fwrite($outputStream, $extra);
+        }
+        if (0 < $commentLength) {
+            // file comment (variable size)
+            fwrite($outputStream, $entry->getComment());
+        }
+        return true;
+    }
+
+    public function release()
+    {
+        unset($this->entries);
+        unset($this->modifiedEntries);
+    }
+
+    function __destruct()
+    {
+        $this->release();
+    }
+
+}

+ 427 - 0
src/PhpZip/Model/EndOfCentralDirectory.php

@@ -0,0 +1,427 @@
+<?php
+namespace PhpZip\Model;
+
+use PhpZip\Exception\InvalidArgumentException;
+use PhpZip\Exception\ZipException;
+use PhpZip\Mapper\OffsetPositionMapper;
+use PhpZip\Mapper\PositionMapper;
+use PhpZip\Util\PackUtil;
+
+/**
+ * Read End of Central Directory
+ *
+ * @author Ne-Lexa alexey@nelexa.ru
+ * @license MIT
+ */
+class EndOfCentralDirectory
+{
+    /** 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 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;
+    /**
+     * @var string|null The archive comment.
+     */
+    private $comment;
+    /**
+     * @var int The number of bytes in the preamble of this ZIP file.
+     */
+    private $preamble;
+    /**
+     * @var int The number of bytes in the postamble of this ZIP file.
+     */
+    private $postamble;
+    /**
+     * @var PositionMapper Maps offsets specified in the ZIP file to real offsets in the file.
+     */
+    private $mapper;
+    /**
+     * @var int
+     */
+    private $centralDirectoryEntriesSize;
+    /**
+     * @var bool
+     */
+    private $zip64 = false;
+    /**
+     * @var string|null
+     */
+    private $newComment;
+    /**
+     * @var bool
+     */
+    private $modified;
+
+    /**
+     * EndOfCentralDirectory constructor.
+     */
+    public function __construct()
+    {
+        $this->mapper = new PositionMapper();
+    }
+
+    /**
+     * Positions the file pointer at the first Central File Header.
+     * Performs some means to check that this is really a ZIP file.
+     *
+     * @param resource $inputStream
+     * @throws ZipException If the file is not compatible to the ZIP File
+     *         Format Specification.
+     */
+    public function findCentralDirectory($inputStream)
+    {
+        // Search for End of central directory record.
+        $stats = fstat($inputStream);
+        $size = $stats['size'];
+        $max = $size - self::END_OF_CENTRAL_DIRECTORY_RECORD_MIN_LEN;
+        $min = $max >= 0xffff ? $max - 0xffff : 0;
+        for ($endOfCentralDirRecordPos = $max; $endOfCentralDirRecordPos >= $min; $endOfCentralDirRecordPos--) {
+            fseek($inputStream, $endOfCentralDirRecordPos, SEEK_SET);
+            // end of central dir signature    4 bytes  (0x06054b50)
+            if (self::END_OF_CENTRAL_DIRECTORY_RECORD_SIG !== unpack('V', fread($inputStream, 4))[1])
+                continue;
+
+            // number of this disk                        - 2 bytes
+            // number of the disk with the start of the
+            //        central directory                   - 2 bytes
+            // total number of entries in the central
+            //        directory on this disk              - 2 bytes
+            // total number of entries in the central
+            //        directory                           - 2 bytes
+            // size of the central directory              - 4 bytes
+            // offset of start of central directory with
+            //        respect to the starting disk number - 4 bytes
+            // ZIP file comment length                    - 2 bytes
+            $data = unpack(
+                'vdiskNo/vcdDiskNo/vcdEntriesDisk/vcdEntries/VcdSize/VcdPos/vcommentLength',
+                fread($inputStream, 18)
+            );
+
+            if (0 !== $data['diskNo'] || 0 !== $data['cdDiskNo'] || $data['cdEntriesDisk'] !== $data['cdEntries']) {
+                throw new ZipException(
+                    "ZIP file spanning/splitting is not supported!"
+                );
+            }
+            // .ZIP file comment       (variable size)
+            if (0 < $data['commentLength']) {
+                $this->comment = fread($inputStream, $data['commentLength']);
+            }
+            $this->preamble = $endOfCentralDirRecordPos;
+            $this->postamble = $size - ftell($inputStream);
+
+            // Check for ZIP64 End Of Central Directory Locator.
+            $endOfCentralDirLocatorPos = $endOfCentralDirRecordPos - self::ZIP64_END_OF_CENTRAL_DIRECTORY_LOCATOR_LEN;
+
+            fseek($inputStream, $endOfCentralDirLocatorPos, SEEK_SET);
+            // zip64 end of central dir locator
+            // signature                       4 bytes  (0x07064b50)
+            if (
+                0 > $endOfCentralDirLocatorPos ||
+                ftell($inputStream) === $size ||
+                self::ZIP64_END_OF_CENTRAL_DIRECTORY_LOCATOR_SIG !== unpack('V', fread($inputStream, 4))[1]
+            ) {
+                // Seek and check first CFH, probably requiring an offset mapper.
+                $offset = $endOfCentralDirRecordPos - $data['cdSize'];
+                fseek($inputStream, $offset, SEEK_SET);
+                $offset -= $data['cdPos'];
+                if (0 !== $offset) {
+                    $this->mapper = new OffsetPositionMapper($offset);
+                }
+                $this->centralDirectoryEntriesSize = $data['cdEntries'];
+                return;
+            }
+
+            // number of the disk with the
+            // start of the zip64 end of
+            // central directory               4 bytes
+            $zip64EndOfCentralDirectoryRecordDisk = unpack('V', fread($inputStream, 4))[1];
+            // relative offset of the zip64
+            // end of central directory record 8 bytes
+            $zip64EndOfCentralDirectoryRecordPos = PackUtil::unpackLongLE(fread($inputStream, 8));
+            // total number of disks           4 bytes
+            $totalDisks = unpack('V', fread($inputStream, 4))[1];
+            if (0 !== $zip64EndOfCentralDirectoryRecordDisk || 1 !== $totalDisks) {
+                throw new ZipException("ZIP file spanning/splitting is not supported!");
+            }
+            fseek($inputStream, $zip64EndOfCentralDirectoryRecordPos, SEEK_SET);
+            // zip64 end of central dir
+            // signature                       4 bytes  (0x06064b50)
+            $zip64EndOfCentralDirSig = unpack('V', fread($inputStream, 4))[1];
+            if (self::ZIP64_END_OF_CENTRAL_DIRECTORY_RECORD_SIG !== $zip64EndOfCentralDirSig) {
+                throw new ZipException("Expected ZIP64 End Of Central Directory Record!");
+            }
+            // size of zip64 end of central
+            // directory record                8 bytes
+            // version made by                 2 bytes
+            // version needed to extract       2 bytes
+            fseek($inputStream, 12, SEEK_CUR);
+            // number of this disk             4 bytes
+            $diskNo = unpack('V', fread($inputStream, 4))[1];
+            // number of the disk with the
+            // start of the central directory  4 bytes
+            $cdDiskNo = unpack('V', fread($inputStream, 4))[1];
+            // total number of entries in the
+            // central directory on this disk  8 bytes
+            $cdEntriesDisk = PackUtil::unpackLongLE(fread($inputStream, 8));
+            // total number of entries in the
+            // central directory               8 bytes
+            $cdEntries = PackUtil::unpackLongLE(fread($inputStream, 8));
+            if (0 !== $diskNo || 0 !== $cdDiskNo || $cdEntriesDisk !== $cdEntries) {
+                throw new ZipException("ZIP file spanning/splitting is not supported!");
+            }
+            if ($cdEntries < 0 || 0x7fffffff < $cdEntries) {
+                throw new ZipException("Total Number Of Entries In The Central Directory out of range!");
+            }
+            // size of the central directory   8 bytes
+            fseek($inputStream, 8, SEEK_CUR);
+            // offset of start of central
+            // directory with respect to
+            // the starting disk number        8 bytes
+            $cdPos = PackUtil::unpackLongLE(fread($inputStream, 8));
+            // zip64 extensible data sector    (variable size)
+            fseek($inputStream, $cdPos, SEEK_SET);
+            $this->preamble = $zip64EndOfCentralDirectoryRecordPos;
+            $this->centralDirectoryEntriesSize = $cdEntries;
+            $this->zip64 = true;
+            return;
+        }
+        // Start recovering file entries from min.
+        $this->preamble = $min;
+        $this->postamble = $size - $min;
+        $this->centralDirectoryEntriesSize = 0;
+    }
+
+    /**
+     * @return null|string
+     */
+    public function getComment()
+    {
+        return $this->comment;
+    }
+
+    /**
+     * @return int
+     */
+    public function getCentralDirectoryEntriesSize()
+    {
+        return $this->centralDirectoryEntriesSize;
+    }
+
+    /**
+     * @return bool
+     */
+    public function isZip64()
+    {
+        return $this->zip64;
+    }
+
+    /**
+     * @return int
+     */
+    public function getPreamble()
+    {
+        return $this->preamble;
+    }
+
+    /**
+     * @return int
+     */
+    public function getPostamble()
+    {
+        return $this->postamble;
+    }
+
+    /**
+     * @return PositionMapper
+     */
+    public function getMapper()
+    {
+        return $this->mapper;
+    }
+
+    /**
+     * @param int $preamble
+     */
+    public function setPreamble($preamble)
+    {
+        $this->preamble = $preamble;
+    }
+
+    /**
+     * Set archive comment
+     * @param string|null $comment
+     * @throws InvalidArgumentException
+     */
+    public function setComment($comment = null)
+    {
+        if (null !== $comment && strlen($comment) !== 0) {
+            $comment = (string)$comment;
+            $length = strlen($comment);
+            if (0x0000 > $length || $length > 0xffff) {
+                throw new InvalidArgumentException('Length comment out of range');
+            }
+        }
+        $this->modified = $comment !== $this->comment;
+        $this->newComment = $comment;
+    }
+
+    /**
+     * @return bool
+     */
+    public function isModified()
+    {
+        return $this->modified;
+    }
+
+    /**
+     * Write end of central directory.
+     *
+     * @param resource $outputStream Output stream
+     * @param int $centralDirectoryEntries Size entries
+     * @param int $centralDirectoryOffset Offset central directory
+     */
+    public function writeEndOfCentralDirectory($outputStream, $centralDirectoryEntries, $centralDirectoryOffset)
+    {
+        $position = ftell($outputStream);
+        $centralDirectorySize = $position - $centralDirectoryOffset;
+        $centralDirectoryEntriesZip64 = $centralDirectoryEntries > 0xffff;
+        $centralDirectorySizeZip64 = $centralDirectorySize > 0xffffffff;
+        $centralDirectoryOffsetZip64 = $centralDirectoryOffset > 0xffffffff;
+        $centralDirectoryEntries16 = $centralDirectoryEntriesZip64 ? 0xffff : (int)$centralDirectoryEntries;
+        $centralDirectorySize32 = $centralDirectorySizeZip64 ? 0xffffffff : $centralDirectorySize;
+        $centralDirectoryOffset32 = $centralDirectoryOffsetZip64 ? 0xffffffff : $centralDirectoryOffset;
+        $zip64 // ZIP64 extensions?
+            = $centralDirectoryEntriesZip64
+            || $centralDirectorySizeZip64
+            || $centralDirectoryOffsetZip64;
+        if ($zip64) {
+            // relative offset of the zip64 end of central directory record
+            $zip64EndOfCentralDirectoryOffset = $position;
+            // zip64 end of central dir
+            // signature                       4 bytes  (0x06064b50)
+            fwrite($outputStream, pack('V', self::ZIP64_END_OF_CENTRAL_DIRECTORY_RECORD_SIG));
+            // size of zip64 end of central
+            // directory record                8 bytes
+            fwrite($outputStream, PackUtil::packLongLE(self::ZIP64_END_OF_CENTRAL_DIRECTORY_RECORD_MIN_LEN - 12));
+            // version made by                 2 bytes
+            // version needed to extract       2 bytes
+            //                                 due to potential use of BZIP2 compression
+            // number of this disk             4 bytes
+            // number of the disk with the
+            // start of the central directory  4 bytes
+            fwrite($outputStream, pack('vvVV', 63, 46, 0, 0));
+            // total number of entries in the
+            // central directory on this disk  8 bytes
+            fwrite($outputStream, PackUtil::packLongLE($centralDirectoryEntries));
+            // total number of entries in the
+            // central directory               8 bytes
+            fwrite($outputStream, PackUtil::packLongLE($centralDirectoryEntries));
+            // size of the central directory   8 bytes
+            fwrite($outputStream, PackUtil::packLongLE($centralDirectorySize));
+            // offset of start of central
+            // directory with respect to
+            // the starting disk number        8 bytes
+            fwrite($outputStream, PackUtil::packLongLE($centralDirectoryOffset));
+            // zip64 extensible data sector    (variable size)
+            //
+            // zip64 end of central dir locator
+            // signature                       4 bytes  (0x07064b50)
+            // number of the disk with the
+            // start of the zip64 end of
+            // central directory               4 bytes
+            fwrite($outputStream, pack('VV', self::ZIP64_END_OF_CENTRAL_DIRECTORY_LOCATOR_SIG, 0));
+            // relative offset of the zip64
+            // end of central directory record 8 bytes
+            fwrite($outputStream, PackUtil::packLongLE($zip64EndOfCentralDirectoryOffset));
+            // total number of disks           4 bytes
+            fwrite($outputStream, pack('V', 1));
+        }
+        $comment = $this->modified ? $this->newComment : $this->comment;
+        $commentLength = strlen($comment);
+        fwrite(
+            $outputStream,
+            pack('VvvvvVVv',
+                // end of central dir signature    4 bytes  (0x06054b50)
+                self::END_OF_CENTRAL_DIRECTORY_RECORD_SIG,
+                // number of this disk             2 bytes
+                0,
+                // number of the disk with the
+                // start of the central directory  2 bytes
+                0,
+                // total number of entries in the
+                // central directory on this disk  2 bytes
+                $centralDirectoryEntries16,
+                // total number of entries in
+                // the central directory           2 bytes
+                $centralDirectoryEntries16,
+                // size of the central directory   4 bytes
+                $centralDirectorySize32,
+                // offset of start of central
+                // directory with respect to
+                // the starting disk number        4 bytes
+                $centralDirectoryOffset32,
+                // .ZIP file comment length        2 bytes
+                $commentLength
+            )
+        );
+        if ($commentLength > 0) {
+            // .ZIP file comment       (variable size)
+            fwrite($outputStream, $comment);
+        }
+    }
+
+}

+ 926 - 0
src/PhpZip/Model/Entry/ZipAbstractEntry.php

@@ -0,0 +1,926 @@
+<?php
+namespace PhpZip\Model\Entry;
+
+use PhpZip\Exception\InvalidArgumentException;
+use PhpZip\Exception\ZipException;
+use PhpZip\Extra\DefaultExtraField;
+use PhpZip\Extra\ExtraField;
+use PhpZip\Extra\ExtraFields;
+use PhpZip\Extra\WinZipAesEntryExtraField;
+use PhpZip\Model\CentralDirectory;
+use PhpZip\Model\ZipEntry;
+use PhpZip\Util\DateTimeConverter;
+use PhpZip\Util\PackUtil;
+use PhpZip\ZipFile;
+
+/**
+ * Abstract ZIP entry.
+ *
+ * @see https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT .ZIP File Format Specification
+ * @author Ne-Lexa alexey@nelexa.ru
+ * @license MIT
+ */
+abstract class ZipAbstractEntry implements ZipEntry
+{
+    /**
+     * @var CentralDirectory
+     */
+    private $centralDirectory;
+
+    /**
+     * @var int Bit flags for init state.
+     */
+    private $init;
+
+    /**
+     * @var string Entry name (filename in archive)
+     */
+    private $name;
+    /**
+     * @var int Made by platform
+     */
+    private $platform;
+    /**
+     * @var int
+     */
+    private $versionNeededToExtract = 20;
+    /**
+     * @var int
+     */
+    private $general;
+    /**
+     * @var int Compression method
+     */
+    private $method;
+    /**
+     * @var int Dos time
+     */
+    private $dosTime;
+    /**
+     * @var int Crc32
+     */
+    private $crc;
+    /**
+     * @var int Compressed size
+     */
+    private $compressedSize = self::UNKNOWN;
+    /**
+     * @var int Uncompressed size
+     */
+    private $size = self::UNKNOWN;
+    /**
+     * @var int External attributes
+     */
+    private $externalAttributes;
+    /**
+     * @var int Relative Offset Of Local File Header.
+     */
+    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;
+    /**
+     * @var string Comment field.
+     */
+    private $comment;
+    /**
+     * @var string Entry password for read or write encryption data.
+     */
+    private $password;
+    /**
+     * Encryption method.
+     * @see ZipFile::ENCRYPTION_METHOD_TRADITIONAL
+     * @see ZipFile::ENCRYPTION_METHOD_WINZIP_AES
+     * @var int
+     */
+    private $encryptionMethod = ZipFile::ENCRYPTION_METHOD_TRADITIONAL;
+
+    /**
+     * @var int
+     */
+    private $compressionLevel = ZipFile::LEVEL_DEFAULT_COMPRESSION;
+
+    /**
+     * @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 CentralDirectory
+     */
+    public function getCentralDirectory()
+    {
+        return $this->centralDirectory;
+    }
+
+    /**
+     * @param CentralDirectory $centralDirectory
+     * @return ZipEntry
+     */
+    public function setCentralDirectory(CentralDirectory $centralDirectory)
+    {
+        $this->centralDirectory = $centralDirectory;
+        return $this;
+    }
+
+    /**
+     * Returns the ZIP entry name.
+     *
+     * @return string
+     */
+    public function getName()
+    {
+        return $this->name;
+    }
+
+    /**
+     * Set entry name.
+     *
+     * @param string $name New entry name
+     * @return ZipEntry
+     * @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;
+        return $this;
+    }
+
+    /**
+     * @return int Get platform
+     */
+    public function getPlatform()
+    {
+        return $this->isInit(self::BIT_PLATFORM) ? $this->platform & 0xffff : self::UNKNOWN;
+    }
+
+    /**
+     * Set platform
+     *
+     * @param int $platform
+     * @return ZipEntry
+     * @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);
+        return $this;
+    }
+
+    /**
+     * Version needed to extract.
+     *
+     * @return int
+     */
+    public function getVersionNeededToExtract()
+    {
+        return $this->versionNeededToExtract;
+    }
+
+    /**
+     * Set version needed to extract.
+     *
+     * @param int $version
+     * @return ZipEntry
+     */
+    public function setVersionNeededToExtract($version)
+    {
+        $this->versionNeededToExtract = $version;
+        return $this;
+    }
+
+    /**
+     * @return bool
+     */
+    public function isZip64ExtensionsRequired()
+    {
+        // 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.
+     * @return ZipEntry
+     * @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;
+        return $this;
+    }
+
+    /**
+     * Returns the uncompressed size of this entry.
+     *
+     * @see ZipEntry::setCompressedSize
+     */
+    public function getSize()
+    {
+        return $this->size;
+    }
+
+    /**
+     * Sets the uncompressed size of this entry.
+     *
+     * @param int $size The (Uncompressed) Size.
+     * @return ZipEntry
+     * @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 $this;
+    }
+
+    /**
+     * Return relative Offset Of Local File Header.
+     *
+     * @return int
+     */
+    public function getOffset()
+    {
+        return $this->offset;
+    }
+
+    /**
+     * @param int $offset
+     * @return ZipEntry
+     * @throws ZipException
+     */
+    public function setOffset($offset)
+    {
+        if (0 > $offset || $offset > 0x7fffffffffffffff) {
+            throw new ZipException("Offset out of range - " . $this->name);
+        }
+        $this->offset = $offset;
+        return $this;
+    }
+
+    /**
+     * Returns true if and only if this ZIP entry represents a directory entry
+     * (i.e. end with '/').
+     *
+     * @return bool
+     */
+    public function isDirectory()
+    {
+        return $this->name[strlen($this->name) - 1] === '/';
+    }
+
+    /**
+     * Returns the General Purpose Bit Flags.
+     *
+     * @return bool
+     */
+    public function getGeneralPurposeBitFlags()
+    {
+        return $this->general & 0xffff;
+    }
+
+    /**
+     * Sets the General Purpose Bit Flags.
+     *
+     * @var int general
+     * @return ZipEntry
+     * @throws ZipException
+     */
+    public function setGeneralPurposeBitFlags($general)
+    {
+        if (0x0000 > $general || $general > 0xffff) {
+            throw new ZipException('general out of range');
+        }
+        $this->general = $general;
+        return $this;
+    }
+
+    /**
+     * Returns the indexed General Purpose Bit Flag.
+     *
+     * @param int $mask
+     * @return bool
+     */
+    public function getGeneralPurposeBitFlag($mask)
+    {
+        return 0 !== ($this->general & $mask);
+    }
+
+    /**
+     * Sets the indexed General Purpose Bit Flag.
+     *
+     * @param int $mask
+     * @param bool $bit
+     * @return ZipEntry
+     */
+    public function setGeneralPurposeBitFlag($mask, $bit)
+    {
+        if ($bit)
+            $this->general |= $mask;
+        else
+            $this->general &= ~$mask;
+        return $this;
+    }
+
+    /**
+     * Returns true if and only if this ZIP entry is encrypted.
+     *
+     * @return bool
+     */
+    public function isEncrypted()
+    {
+        return $this->getGeneralPurposeBitFlag(self::GPBF_ENCRYPTED);
+    }
+
+    /**
+     * Sets the encryption property to false and removes any other
+     * encryption artifacts.
+     *
+     * @return ZipEntry
+     */
+    public function clearEncryption()
+    {
+        $this->setEncrypted(false);
+        if (null !== $this->fields) {
+            $field = $this->fields->get(WinZipAesEntryExtraField::getHeaderId());
+            if (null !== $field) {
+                /**
+                 * @var WinZipAesEntryExtraField $field
+                 */
+                $this->removeExtraField(WinZipAesEntryExtraField::getHeaderId());
+            }
+            if (self::METHOD_WINZIP_AES === $this->getMethod()) {
+                $this->setMethod(null === $field ? self::UNKNOWN : $field->getMethod());
+            }
+        }
+        $this->password = null;
+        return $this;
+    }
+
+    /**
+     * Sets the encryption flag for this ZIP entry.
+     *
+     * @param bool $encrypted
+     * @return ZipEntry
+     */
+    public function setEncrypted($encrypted)
+    {
+        $this->setGeneralPurposeBitFlag(self::GPBF_ENCRYPTED, $encrypted);
+        return $this;
+    }
+
+    /**
+     * 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
+     * @return ZipEntry
+     * @throws ZipException If method is not STORED, DEFLATED, BZIP2 or UNKNOWN.
+     */
+    public function setMethod($method)
+    {
+        if (0x0000 > $method || $method > 0xffff) {
+            throw new ZipException('method out of range');
+        }
+        switch ($method) {
+            case self::METHOD_WINZIP_AES:
+                $this->method = $method;
+                $this->setInit(self::BIT_METHOD, true);
+                $this->setEncryptionMethod(ZipFile::ENCRYPTION_METHOD_WINZIP_AES);
+                break;
+
+            case ZipFile::METHOD_STORED:
+            case ZipFile::METHOD_DEFLATED:
+            case ZipFile::METHOD_BZIP2:
+                $this->method = $method;
+                $this->setInit(self::BIT_METHOD, true);
+                break;
+
+            case self::UNKNOWN:
+                $this->method = ZipFile::METHOD_STORED;
+                $this->setInit(self::BIT_METHOD, false);
+                break;
+
+            default:
+                throw new ZipException($this->name . " (unsupported compression method $method)");
+        }
+        return $this;
+    }
+
+    /**
+     * 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
+     * @return ZipEntry
+     */
+    public function setTime($unixTimestamp)
+    {
+        $known = self::UNKNOWN != $unixTimestamp;
+        if ($known) {
+            $this->dosTime = DateTimeConverter::toDosTime($unixTimestamp);
+        } else {
+            $this->dosTime = 0;
+        }
+        $this->setInit(self::BIT_DATE_TIME, $known);
+        return $this;
+    }
+
+    /**
+     * Returns the external file attributes.
+     *
+     * @return int The external file attributes.
+     */
+    public function getExternalAttributes()
+    {
+        if (!$this->isInit(self::BIT_EXTERNAL_ATTR)) {
+            return $this->isDirectory() ? 0x10 : 0;
+        }
+        return $this->externalAttributes & 0xffffffff;
+    }
+
+    /**
+     * Sets the external file attributes.
+     *
+     * @param int $externalAttributes the external file attributes.
+     * @return ZipEntry
+     * @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 $this;
+    }
+
+    /**
+     * 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);
+    }
+
+    /**
+     * Add extra field.
+     *
+     * @param ExtraField $field
+     * @return ExtraField
+     * @throws ZipException
+     */
+    public function addExtraField($field)
+    {
+        if (null === $field) {
+            throw new ZipException("extra field null");
+        }
+        if (null === $this->fields) {
+            $this->fields = new ExtraFields();
+        }
+        return $this->fields->add($field);
+    }
+
+    /**
+     * Return exists extra field from header id.
+     *
+     * @param int $headerId
+     * @return bool
+     */
+    public function hasExtraField($headerId)
+    {
+        return $this->fields === null ? false : $this->fields->has($headerId);
+    }
+
+    /**
+     * 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;
+    }
+
+    /**
+     * Returns a protective copy of the serialized Extra Fields.
+     *
+     * @return string A new byte array holding the serialized Extra Fields.
+     *                null is never returned.
+     */
+    public function getExtra()
+    {
+        return $this->getExtraFields(false);
+    }
+
+    /**
+     * @param bool $zip64
+     * @return string
+     * @throws ZipException
+     */
+    private function getExtraFields($zip64)
+    {
+        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()
+    {
+        $handle = fopen('php://memory', 'r+b');
+        // Write out Uncompressed Size.
+        $size = $this->getSize();
+        if (0xffffffff <= $size) {
+            fwrite($handle, PackUtil::packLongLE($size));
+        }
+        // Write out Compressed Size.
+        $compressedSize = $this->getCompressedSize();
+        if (0xffffffff <= $compressedSize) {
+            fwrite($handle, PackUtil::packLongLE($compressedSize));
+        }
+        // Write out Relative Header Offset.
+        $offset = $this->getOffset();
+        if (0xffffffff <= $offset) {
+            fwrite($handle, PackUtil::packLongLE($offset));
+        }
+        // Create ZIP64 Extended Information Extra Field from serialized data.
+        $field = null;
+        if (ftell($handle) > 0) {
+            $field = new DefaultExtraField(ExtraField::ZIP64_HEADER_ID);
+            $field->readFrom($handle, 0, ftell($handle));
+        } else {
+            $field = null;
+        }
+        return $field;
+    }
+
+    /**
+     * Sets the serialized Extra Fields by making a protective copy.
+     * Note that this method parses the serialized Extra Fields according to
+     * the ZIP File Format Specification and limits its size to 64 KB.
+     * Therefore, this property cannot not be used to hold arbitrary
+     * (application) data.
+     * Consider storing such data in a separate entry instead.
+     *
+     * @param string $data The byte array holding the serialized Extra Fields.
+     * @throws ZipException if the serialized Extra Fields exceed 64 KB
+     * @return ZipEntry
+     *         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);
+        }
+        return $this;
+    }
+
+    /**
+     * @param string $data
+     * @param bool $zip64
+     */
+    private function setExtraFields($data, $zip64)
+    {
+        if (null === $this->fields) {
+            $this->fields = new ExtraFields();
+        }
+        $handle = fopen('php://memory', 'r+b');
+        fwrite($handle, $data);
+        rewind($handle);
+
+        $this->fields->readFrom($handle, 0, strlen($data));
+        $result = false;
+        if ($zip64) {
+            $result = $this->parseZip64ExtraField();
+        }
+        if ($result) {
+            $this->fields->remove(ExtraField::ZIP64_HEADER_ID);
+            if ($this->fields->size() <= 0) {
+                if (0 !== $this->fields->size()) {
+                    $this->fields = null;
+                }
+            }
+        }
+        fclose($handle);
+    }
+
+    /**
+     * Parses the properties of this entry from the ZIP64 Extended Information
+     * Extra Field, if present.
+     * The ZIP64 Extended Information Extra Field is not removed.
+     *
+     * @return bool
+     * @throws ZipException
+     */
+    private function parseZip64ExtraField()
+    {
+        if (null === $this->fields) {
+            return false;
+        }
+        $ef = $this->fields->get(ExtraField::ZIP64_HEADER_ID);
+        if (null === $ef) {
+            return false;
+        }
+        $dataBlockHandle = $ef->getDataBlock();
+        $off = 0;
+        // Read in Uncompressed Size.
+        $size = $this->getSize();
+        if (0xffffffff <= $size) {
+            assert(0xffffffff === $size);
+            fseek($dataBlockHandle, $off);
+            $this->setSize(PackUtil::unpackLongLE(fread($dataBlockHandle, 8)));
+            $off += 8;
+        }
+        // Read in Compressed Size.
+        $compressedSize = $this->getCompressedSize();
+        if (0xffffffff <= $compressedSize) {
+            assert(0xffffffff === $compressedSize);
+            fseek($dataBlockHandle, $off);
+            $this->setCompressedSize(PackUtil::unpackLongLE(fread($dataBlockHandle, 8)));
+            $off += 8;
+        }
+        // Read in Relative Header Offset.
+        $offset = $this->getOffset();
+        if (0xffffffff <= $offset) {
+            assert(0xffffffff, $offset);
+            fseek($dataBlockHandle, $off);
+            $this->setOffset(PackUtil::unpackLongLE(fread($dataBlockHandle, 8)));
+            //$off += 8;
+        }
+        fclose($dataBlockHandle);
+        return true;
+    }
+
+    /**
+     * Returns comment entry
+     *
+     * @return string
+     */
+    public function getComment()
+    {
+        return null != $this->comment ? $this->comment : "";
+    }
+
+    /**
+     * Set entry comment.
+     *
+     * @param $comment
+     * @return ZipEntry
+     * @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 $this;
+    }
+
+    /**
+     * @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->crc & 0xffffffff;
+    }
+
+    /**
+     * Set crc32 content.
+     *
+     * @param int $crc
+     * @return ZipEntry
+     * @throws ZipException
+     */
+    public function setCrc($crc)
+    {
+        if (0x00000000 > $crc || $crc > 0xffffffff) {
+            throw new ZipException("CRC-32 out of range - " . $this->name);
+        }
+        $this->crc = $crc;
+        $this->setInit(self::BIT_CRC, true);
+        return $this;
+    }
+
+    /**
+     * @return string
+     */
+    public function getPassword()
+    {
+        return $this->password;
+    }
+
+    /**
+     * Set password and encryption method from entry
+     *
+     * @param string $password
+     * @param null|int $encryptionMethod
+     * @return ZipEntry
+     */
+    public function setPassword($password, $encryptionMethod = null)
+    {
+        $this->password = $password;
+        if ($encryptionMethod !== null) {
+            $this->setEncryptionMethod($encryptionMethod);
+        }
+        $this->setEncrypted(!empty($this->password));
+        return $this;
+    }
+
+    /**
+     * @return int
+     */
+    public function getEncryptionMethod()
+    {
+        return $this->encryptionMethod;
+    }
+
+    /**
+     * @return int
+     */
+    public function getCompressionLevel()
+    {
+        return $this->compressionLevel;
+    }
+
+    /**
+     * @param int $compressionLevel
+     * @return ZipEntry
+     * @throws InvalidArgumentException
+     */
+    public function setCompressionLevel($compressionLevel = ZipFile::LEVEL_DEFAULT_COMPRESSION)
+    {
+        if ($compressionLevel < ZipFile::LEVEL_DEFAULT_COMPRESSION ||
+            $compressionLevel > ZipFile::LEVEL_BEST_COMPRESSION
+        ) {
+            throw new InvalidArgumentException('Invalid compression level. Minimum level ' .
+                ZipFile::LEVEL_DEFAULT_COMPRESSION . '. Maximum level ' . ZipFile::LEVEL_BEST_COMPRESSION);
+        }
+        $this->compressionLevel = $compressionLevel;
+        return $this;
+    }
+
+    /**
+     * Set encryption method
+     *
+     * @see ZipFile::ENCRYPTION_METHOD_TRADITIONAL
+     * @see ZipFile::ENCRYPTION_METHOD_WINZIP_AES
+     *
+     * @param int $encryptionMethod
+     * @return ZipEntry
+     * @throws ZipException
+     */
+    public function setEncryptionMethod($encryptionMethod)
+    {
+        if (
+            ZipFile::ENCRYPTION_METHOD_TRADITIONAL !== $encryptionMethod &&
+            ZipFile::ENCRYPTION_METHOD_WINZIP_AES !== $encryptionMethod
+        ) {
+            throw new ZipException('Invalid encryption method');
+        }
+        $this->encryptionMethod = $encryptionMethod;
+        $this->setEncrypted(true);
+        return $this;
+    }
+
+    /**
+     * Clone extra fields
+     */
+    function __clone()
+    {
+        $this->fields = $this->fields !== null ? clone $this->fields : null;
+    }
+}

+ 26 - 0
src/PhpZip/Model/Entry/ZipNewEmptyDirEntry.php

@@ -0,0 +1,26 @@
+<?php
+namespace PhpZip\Model\Entry;
+
+use PhpZip\Exception\ZipException;
+
+/**
+ * New zip entry from empty dir.
+ *
+ * @see https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT .ZIP File Format Specification
+ * @author Ne-Lexa alexey@nelexa.ru
+ * @license MIT
+ */
+class ZipNewEmptyDirEntry extends ZipNewEntry
+{
+
+    /**
+     * Returns an string content of the given entry.
+     *
+     * @return null|string
+     * @throws ZipException
+     */
+    public function getEntryContent()
+    {
+        return null;
+    }
+}

+ 268 - 0
src/PhpZip/Model/Entry/ZipNewEntry.php

@@ -0,0 +1,268 @@
+<?php
+namespace PhpZip\Model\Entry;
+
+use PhpZip\Crypto\TraditionalPkwareEncryptionEngine;
+use PhpZip\Crypto\WinZipAesEngine;
+use PhpZip\Exception\ZipException;
+use PhpZip\Extra\WinZipAesEntryExtraField;
+use PhpZip\Model\ZipEntry;
+use PhpZip\Util\PackUtil;
+use PhpZip\ZipFile;
+
+/**
+ * Abstract class for new zip entry.
+ *
+ * @see https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT .ZIP File Format Specification
+ * @author Ne-Lexa alexey@nelexa.ru
+ * @license MIT
+ */
+abstract class ZipNewEntry extends ZipAbstractEntry
+{
+    /**
+     * Default compression level for bzip2
+     */
+    const LEVEL_DEFAULT_BZIP2_COMPRESSION = 4;
+
+    /**
+     * Version needed to extract.
+     *
+     * @return int
+     */
+    public function getVersionNeededToExtract()
+    {
+        $method = $this->getMethod();
+        return self::METHOD_WINZIP_AES === $method ? 51 :
+            (ZipFile::METHOD_BZIP2 === $method ? 46 :
+                ($this->isZip64ExtensionsRequired() ? 45 :
+                    (ZipFile::METHOD_DEFLATED === $method || $this->isDirectory() ? 20 : 10)
+                )
+            );
+    }
+
+    /**
+     * Write local file header, encryption header, file data and data descriptor to output stream.
+     *
+     * @param resource $outputStream
+     * @throws ZipException
+     */
+    public function writeEntry($outputStream)
+    {
+        $nameLength = strlen($this->getName());
+        $size = $nameLength + strlen($this->getExtra()) + strlen($this->getComment());
+        if (0xffff < $size) {
+            throw new ZipException($this->getName()
+                . " (the total size of "
+                . $size
+                . " bytes for the name, extra fields and comment exceeds the maximum size of "
+                . 0xffff . " bytes)");
+        }
+
+        if (self::UNKNOWN === $this->getPlatform()) {
+            $this->setPlatform(self::PLATFORM_UNIX);
+        }
+        if (self::UNKNOWN === $this->getTime()) {
+            $this->setTime(time());
+        }
+        $method = $this->getMethod();
+        if (self::UNKNOWN === $method) {
+            $this->setMethod($method = ZipFile::METHOD_DEFLATED);
+        }
+        $skipCrc = false;
+
+        $encrypted = $this->isEncrypted();
+        $dd = $this->isDataDescriptorRequired();
+        // Compose General Purpose Bit Flag.
+        // See appendix D of PKWARE's ZIP File Format Specification.
+        $utf8 = true;
+        $general = ($encrypted ? self::GPBF_ENCRYPTED : 0)
+            | ($dd ? self::GPBF_DATA_DESCRIPTOR : 0)
+            | ($utf8 ? self::GPBF_UTF8 : 0);
+
+        $entryContent = $this->getEntryContent();
+
+        $this->setSize(strlen($entryContent));
+        $this->setCrc(crc32($entryContent));
+
+        if ($encrypted && null === $this->getPassword()) {
+            throw new ZipException("Can not password from entry " . $this->getName());
+        }
+
+        if (
+            $encrypted &&
+            (
+                self::METHOD_WINZIP_AES === $method ||
+                $this->getEncryptionMethod() === ZipFile::ENCRYPTION_METHOD_WINZIP_AES
+            )
+        ) {
+            $field = null;
+            $method = $this->getMethod();
+            $keyStrength = 256; // bits
+
+            $compressedSize = $this->getCompressedSize();
+
+            if (self::METHOD_WINZIP_AES === $method) {
+                /**
+                 * @var WinZipAesEntryExtraField $field
+                 */
+                $field = $this->getExtraField(WinZipAesEntryExtraField::getHeaderId());
+                if (null !== $field) {
+                    $method = $field->getMethod();
+                    if (self::UNKNOWN !== $compressedSize) {
+                        $compressedSize -= $field->getKeyStrength() / 2 // salt value
+                            + 2   // password verification value
+                            + 10; // authentication code
+                    }
+                    $this->setMethod($method);
+                }
+            }
+            if (null === $field) {
+                $field = new WinZipAesEntryExtraField();
+            }
+            $field->setKeyStrength($keyStrength);
+            $field->setMethod($method);
+            $size = $this->getSize();
+            if (20 <= $size && ZipFile::METHOD_BZIP2 !== $method) {
+                $field->setVendorVersion(WinZipAesEntryExtraField::VV_AE_1);
+            } else {
+                $field->setVendorVersion(WinZipAesEntryExtraField::VV_AE_2);
+                $skipCrc = true;
+            }
+            $this->addExtraField($field);
+            if (self::UNKNOWN !== $compressedSize) {
+                $compressedSize += $field->getKeyStrength() / 2 // salt value
+                    + 2   // password verification value
+                    + 10; // authentication code
+                $this->setCompressedSize($compressedSize);
+            }
+            if ($skipCrc) {
+                $this->setCrc(0);
+            }
+        }
+
+        switch ($method) {
+            case ZipFile::METHOD_STORED:
+                break;
+            case ZipFile::METHOD_DEFLATED:
+                $entryContent = gzdeflate($entryContent, $this->getCompressionLevel());
+                break;
+            case ZipFile::METHOD_BZIP2:
+                $compressionLevel = $this->getCompressionLevel() === ZipFile::LEVEL_DEFAULT_COMPRESSION ?
+                    self::LEVEL_DEFAULT_BZIP2_COMPRESSION :
+                    $this->getCompressionLevel();
+                $entryContent = bzcompress($entryContent, $compressionLevel);
+                if (is_int($entryContent)) {
+                    throw new ZipException('Error bzip2 compress. Error code: ' . $entryContent);
+                }
+                break;
+            default:
+                throw new ZipException($this->getName() . " (unsupported compression method " . $method . ")");
+        }
+
+        if ($encrypted) {
+            if ($this->getEncryptionMethod() === ZipFile::ENCRYPTION_METHOD_WINZIP_AES) {
+                if ($skipCrc) {
+                    $this->setCrc(0);
+                }
+                $this->setMethod(self::METHOD_WINZIP_AES);
+
+                /**
+                 * @var WinZipAesEntryExtraField $field
+                 */
+                $field = $this->getExtraField(WinZipAesEntryExtraField::getHeaderId());
+                $winZipAesEngine = new WinZipAesEngine($this, $field);
+                $entryContent = $winZipAesEngine->encrypt($entryContent);
+            } elseif ($this->getEncryptionMethod() === ZipFile::ENCRYPTION_METHOD_TRADITIONAL) {
+                $zipCryptoEngine = new TraditionalPkwareEncryptionEngine($this);
+                $entryContent = $zipCryptoEngine->encrypt($entryContent);
+            }
+        }
+
+        $compressedSize = strlen($entryContent);
+        $this->setCompressedSize($compressedSize);
+
+        $offset = ftell($outputStream);
+
+        // Commit changes.
+        $this->setGeneralPurposeBitFlags($general);
+        $this->setOffset($offset);
+
+        $extra = $this->getExtra();
+
+        // zip align
+        $padding = 0;
+        $zipAlign = $this->getCentralDirectory()->getZipAlign();
+        $extraLength = strlen($extra);
+        if ($zipAlign !== null && !$this->isEncrypted() && $this->getMethod() === ZipFile::METHOD_STORED) {
+            $padding =
+                (
+                    $zipAlign -
+                    (
+                        $offset +
+                        ZipEntry::LOCAL_FILE_HEADER_MIN_LEN +
+                        $nameLength + $extraLength
+                    ) % $zipAlign
+                ) % $zipAlign;
+        }
+
+        fwrite(
+            $outputStream,
+            pack(
+                'VvvvVVVVvv',
+                // local file header signature     4 bytes  (0x04034b50)
+                self::LOCAL_FILE_HEADER_SIG,
+                // version needed to extract       2 bytes
+                $this->getVersionNeededToExtract(),
+                // general purpose bit flag        2 bytes
+                $general,
+                // compression method              2 bytes
+                $this->getMethod(),
+                // last mod file time              2 bytes
+                // last mod file date              2 bytes
+                $this->getTime(),
+                // crc-32                          4 bytes
+                $dd ? 0 : $this->getCrc(),
+                // compressed size                 4 bytes
+                $dd ? 0 : $this->getCompressedSize(),
+                // uncompressed size               4 bytes
+                $dd ? 0 : $this->getSize(),
+                // file name length                2 bytes
+                $nameLength,
+                // extra field length              2 bytes
+                $extraLength + $padding
+            )
+        );
+        fwrite($outputStream, $this->getName());
+        if ($extraLength > 0) {
+            fwrite($outputStream, $extra);
+        }
+
+        if ($padding > 0) {
+            fwrite($outputStream, str_repeat(chr(0), $padding));
+        }
+
+        if ($entryContent !== null) {
+            fwrite($outputStream, $entryContent);
+        }
+
+        assert(self::UNKNOWN !== $this->getCrc());
+        assert(self::UNKNOWN !== $this->getSize());
+        if ($this->getGeneralPurposeBitFlag(self::GPBF_DATA_DESCRIPTOR)) {
+            // data descriptor signature       4 bytes  (0x08074b50)
+            // crc-32                          4 bytes
+            fwrite($outputStream, pack('VV', self::DATA_DESCRIPTOR_SIG, $this->getCrc()));
+            // compressed size                 4 or 8 bytes
+            // uncompressed size               4 or 8 bytes
+            if ($this->isZip64ExtensionsRequired()) {
+                fwrite($outputStream, PackUtil::packLongLE($compressedSize));
+                fwrite($outputStream, PackUtil::packLongLE($this->getSize()));
+            } else {
+                fwrite($outputStream, pack('VV', $this->getCompressedSize(), $this->getSize()));
+            }
+        } elseif ($this->getCompressedSize() !== $compressedSize) {
+            throw new ZipException($this->getName()
+                . " (expected compressed entry size of "
+                . $this->getCompressedSize() . " bytes, but is actually " . $compressedSize . " bytes)");
+        }
+    }
+
+}

+ 55 - 0
src/PhpZip/Model/Entry/ZipNewStreamEntry.php

@@ -0,0 +1,55 @@
+<?php
+namespace PhpZip\Model\Entry;
+
+use PhpZip\Exception\InvalidArgumentException;
+use PhpZip\Exception\ZipException;
+
+/**
+ * New zip entry from stream.
+ *
+ * @see https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT .ZIP File Format Specification
+ * @author Ne-Lexa alexey@nelexa.ru
+ * @license MIT
+ */
+class ZipNewStreamEntry extends ZipNewEntry
+{
+    /**
+     * @var resource
+     */
+    private $stream;
+
+    /**
+     * ZipNewStreamEntry constructor.
+     * @param resource $stream
+     * @throws InvalidArgumentException
+     */
+    public function __construct($stream)
+    {
+        if (!is_resource($stream)) {
+            throw new InvalidArgumentException('stream is not resource');
+        }
+        $this->stream = $stream;
+    }
+
+    /**
+     * Returns an string content of the given entry.
+     *
+     * @return null|string
+     * @throws ZipException
+     */
+    public function getEntryContent()
+    {
+        return stream_get_contents($this->stream, -1, 0);
+    }
+
+    /**
+     * Release stream resource.
+     */
+    function __destruct()
+    {
+        if ($this->stream !== null) {
+            fclose($this->stream);
+            $this->stream = null;
+        }
+    }
+}

+ 39 - 0
src/PhpZip/Model/Entry/ZipNewStringEntry.php

@@ -0,0 +1,39 @@
+<?php
+namespace PhpZip\Model\Entry;
+
+use PhpZip\Exception\ZipException;
+
+/**
+ * New zip entry from string.
+ *
+ * @see https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT .ZIP File Format Specification
+ * @author Ne-Lexa alexey@nelexa.ru
+ * @license MIT
+ */
+class ZipNewStringEntry extends ZipNewEntry
+{
+    /**
+     * @var string
+     */
+    private $entryContent;
+
+    /**
+     * ZipNewStringEntry constructor.
+     * @param string $entryContent
+     */
+    public function __construct($entryContent)
+    {
+        $this->entryContent = $entryContent;
+    }
+
+    /**
+     * Returns an string content of the given entry.
+     *
+     * @return null|string
+     * @throws ZipException
+     */
+    public function getEntryContent()
+    {
+        return $this->entryContent;
+    }
+}

+ 323 - 0
src/PhpZip/Model/Entry/ZipReadEntry.php

@@ -0,0 +1,323 @@
+<?php
+namespace PhpZip\Model\Entry;
+
+use PhpZip\Crypto\TraditionalPkwareEncryptionEngine;
+use PhpZip\Crypto\WinZipAesEngine;
+use PhpZip\Exception\Crc32Exception;
+use PhpZip\Exception\InvalidArgumentException;
+use PhpZip\Exception\ZipCryptoException;
+use PhpZip\Exception\ZipException;
+use PhpZip\Exception\ZipUnsupportMethod;
+use PhpZip\Extra\WinZipAesEntryExtraField;
+use PhpZip\Model\CentralDirectory;
+use PhpZip\Model\ZipEntry;
+use PhpZip\ZipFile;
+
+/**
+ * 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 ZipReadEntry extends ZipAbstractEntry
+{
+    /**
+     * Max size cached content in memory.
+     */
+    const MAX_SIZE_CACHED_CONTENT_IN_MEMORY = 3145728; // 3 mb
+    /**
+     * @var resource
+     */
+    private $inputStream;
+    /**
+     * @var string
+     */
+    private $charset;
+    /**
+     * @var string|resource Cached entry content.
+     */
+    private $entryContent;
+
+    /**
+     * ZipFileEntry constructor.
+     * @param $inputStream
+     */
+    public function __construct($inputStream)
+    {
+        $this->inputStream = $inputStream;
+        $this->readZipEntry($inputStream);
+    }
+
+    /**
+     * @param resource $inputStream
+     * @throws InvalidArgumentException
+     */
+    private function readZipEntry($inputStream)
+    {
+        // central file header signature   4 bytes  (0x02014b50)
+        $fileHeaderSig = unpack('V', fread($inputStream, 4))[1];
+        if (CentralDirectory::CENTRAL_FILE_HEADER_SIG !== $fileHeaderSig) {
+            throw new InvalidArgumentException("Corrupt zip file. Can not read zip entry.");
+        }
+
+        // version made by                 2 bytes
+        // version needed to extract       2 bytes
+        // general purpose bit flag        2 bytes
+        // compression method              2 bytes
+        // last mod file time              2 bytes
+        // last mod file date              2 bytes
+        // crc-32                          4 bytes
+        // compressed size                 4 bytes
+        // uncompressed size               4 bytes
+        // file name length                2 bytes
+        // extra field length              2 bytes
+        // file comment length             2 bytes
+        // disk number start               2 bytes
+        // internal file attributes        2 bytes
+        // external file attributes        4 bytes
+        // relative offset of local header 4 bytes
+        $data = unpack(
+            'vversionMadeBy/vversionNeededToExtract/vgpbf/vrawMethod/VrawTime/VrawCrc/VrawCompressedSize/' .
+            'VrawSize/vfileLength/vextraLength/vcommentLength/VrawInternalAttributes/VrawExternalAttributes/VlfhOff',
+            fread($inputStream, 42)
+        );
+
+        $utf8 = 0 !== ($data['gpbf'] & self::GPBF_UTF8);
+        if ($utf8) {
+            $this->charset = "UTF-8";
+        }
+
+        // See appendix D of PKWARE's ZIP File Format Specification.
+        $name = fread($inputStream, $data['fileLength']);
+
+        $this->setName($name);
+        $this->setVersionNeededToExtract($data['versionNeededToExtract']);
+        $this->setPlatform($data['versionMadeBy'] >> 8);
+        $this->setGeneralPurposeBitFlags($data['gpbf']);
+        $this->setMethod($data['rawMethod']);
+        $this->setTime($data['rawTime']);
+        $this->setCrc($data['rawCrc']);
+        $this->setCompressedSize($data['rawCompressedSize']);
+        $this->setSize($data['rawSize']);
+        $this->setExternalAttributes($data['rawExternalAttributes']);
+        $this->setOffset($data['lfhOff']); // must be unmapped!
+        if (0 < $data['extraLength']) {
+            $this->setExtra(fread($inputStream, $data['extraLength']));
+        }
+        if (0 < $data['commentLength']) {
+            $this->setComment(fread($inputStream, $data['commentLength']));
+        }
+    }
+
+    /**
+     * Returns an string content of the given entry.
+     *
+     * @return string
+     * @throws ZipException
+     */
+    public function getEntryContent()
+    {
+        if ($this->entryContent === null) {
+            $isEncrypted = $this->isEncrypted();
+            $password = $this->getPassword();
+            if ($isEncrypted && empty($password)) {
+                throw new ZipException("Not set password");
+            }
+
+            $pos = $this->getOffset();
+            assert(self::UNKNOWN !== $pos);
+            $startPos = $pos = $this->getCentralDirectory()->getEndOfCentralDirectory()->getMapper()->map($pos);
+            fseek($this->inputStream, $startPos);
+
+            // local file header signature     4 bytes  (0x04034b50)
+            if (self::LOCAL_FILE_HEADER_SIG !== unpack('V', fread($this->inputStream, 4))[1]) {
+                throw new ZipException($this->getName() . " (expected Local File Header)");
+            }
+            fseek($this->inputStream, $pos + ZipEntry::LOCAL_FILE_HEADER_FILE_NAME_LENGTH_POS);
+            // file name length                2 bytes
+            // extra field length              2 bytes
+            $data = unpack('vfileLength/vextraLength', fread($this->inputStream, 4));
+            $pos += ZipEntry::LOCAL_FILE_HEADER_MIN_LEN + $data['fileLength'] + $data['extraLength'];
+
+            assert(self::UNKNOWN !== $this->getCrc());
+
+            $method = $this->getMethod();
+
+            fseek($this->inputStream, $pos);
+
+            // Get raw entry content
+            $content = fread($this->inputStream, $this->getCompressedSize());
+
+            // Strong Encryption Specification - WinZip AES
+            if ($this->isEncrypted()) {
+                if (self::METHOD_WINZIP_AES === $method) {
+                    $winZipAesEngine = new WinZipAesEngine($this);
+                    $content = $winZipAesEngine->decrypt($content);
+                    // Disable redundant CRC-32 check.
+                    $isEncrypted = false;
+
+                    /**
+                     * @var WinZipAesEntryExtraField $field
+                     */
+                    $field = $this->getExtraField(WinZipAesEntryExtraField::getHeaderId());
+                    $method = $field->getMethod();
+                    $this->setEncryptionMethod(ZipFile::ENCRYPTION_METHOD_WINZIP_AES);
+                } else {
+                    // Traditional PKWARE Decryption
+                    $zipCryptoEngine = new TraditionalPkwareEncryptionEngine($this);
+                    $content = $zipCryptoEngine->decrypt($content);
+
+                    $this->setEncryptionMethod(ZipFile::ENCRYPTION_METHOD_TRADITIONAL);
+                }
+            }
+            if ($isEncrypted) {
+                // Check CRC32 in the Local File Header or Data Descriptor.
+                $localCrc = null;
+                if ($this->getGeneralPurposeBitFlag(self::GPBF_DATA_DESCRIPTOR)) {
+                    // The CRC32 is in the Data Descriptor after the compressed size.
+                    // Note the Data Descriptor's Signature is optional:
+                    // All newer apps should write it (and so does TrueVFS),
+                    // but older apps might not.
+                    fseek($this->inputStream, $pos + $this->getCompressedSize());
+                    $localCrc = unpack('V', fread($this->inputStream, 4))[1];
+                    if (self::DATA_DESCRIPTOR_SIG === $localCrc) {
+                        $localCrc = unpack('V', fread($this->inputStream, 4))[1];
+                    }
+                } else {
+                    fseek($this->inputStream, $startPos + 14);
+                    // The CRC32 in the Local File Header.
+                    $localCrc = unpack('V', fread($this->inputStream, 4))[1];
+                }
+                if ($this->getCrc() !== $localCrc) {
+                    throw new Crc32Exception($this->getName(), $this->getCrc(), $localCrc);
+                }
+            }
+
+            switch ($method) {
+                case ZipFile::METHOD_STORED:
+                    break;
+                case ZipFile::METHOD_DEFLATED:
+                    $content = gzinflate($content);
+                    break;
+                case ZipFile::METHOD_BZIP2:
+                    if (!extension_loaded('bz2')) {
+                        throw new ZipException('Extension bzip2 not install');
+                    }
+                    $content = bzdecompress($content);
+                    break;
+                default:
+                    throw new ZipUnsupportMethod($this->getName()
+                        . " (compression method "
+                        . $method
+                        . " is not supported)");
+            }
+            if ($isEncrypted) {
+                $localCrc = crc32($content);
+                if ($this->getCrc() !== $localCrc) {
+                    if ($this->isEncrypted()) {
+                        throw new ZipCryptoException("Wrong password");
+                    }
+                    throw new Crc32Exception($this->getName(), $this->getCrc(), $localCrc);
+                }
+            }
+            if ($this->getSize() < self::MAX_SIZE_CACHED_CONTENT_IN_MEMORY) {
+                $this->entryContent = $content;
+            } else {
+                $this->entryContent = fopen('php://temp', 'rb');
+                fwrite($this->entryContent, $content);
+            }
+            return $content;
+        }
+        if (is_resource($this->entryContent)) {
+            return stream_get_contents($this->entryContent, -1, 0);
+        }
+        return $this->entryContent;
+    }
+
+    /**
+     * Write local file header, encryption header, file data and data descriptor to output stream.
+     *
+     * @param resource $outputStream
+     */
+    public function writeEntry($outputStream)
+    {
+        $pos = $this->getOffset();
+        assert(ZipEntry::UNKNOWN !== $pos);
+        $pos = $this->getCentralDirectory()->getEndOfCentralDirectory()->getMapper()->map($pos);
+        $pos += ZipEntry::LOCAL_FILE_HEADER_FILE_NAME_LENGTH_POS;
+
+        $this->setOffset(ftell($outputStream));
+        // zip align
+        $padding = 0;
+        $zipAlign = $this->getCentralDirectory()->getZipAlign();
+        $extra = $this->getExtra();
+        $extraLength = strlen($extra);
+        $nameLength = strlen($this->getName());
+        if ($zipAlign !== null && !$this->isEncrypted() && $this->getMethod() === ZipFile::METHOD_STORED) {
+            $padding =
+                (
+                    $zipAlign -
+                    ($this->getOffset() + ZipEntry::LOCAL_FILE_HEADER_MIN_LEN + $nameLength + $extraLength)
+                    % $zipAlign
+                ) % $zipAlign;
+        }
+        $dd = $this->isDataDescriptorRequired();
+
+        fwrite(
+            $outputStream,
+            pack(
+                'VvvvVVVVvv',
+                // local file header signature     4 bytes  (0x04034b50)
+                self::LOCAL_FILE_HEADER_SIG,
+                // version needed to extract       2 bytes
+                $this->getVersionNeededToExtract(),
+                // general purpose bit flag        2 bytes
+                $this->getGeneralPurposeBitFlags(),
+                // compression method              2 bytes
+                $this->getMethod(),
+                // last mod file time              2 bytes
+                // last mod file date              2 bytes
+                $this->getTime(),
+                // crc-32                          4 bytes
+                $dd ? 0 : $this->getCrc(),
+                // compressed size                 4 bytes
+                $dd ? 0 : $this->getCompressedSize(),
+                // uncompressed size               4 bytes
+                $dd ? 0 : $this->getSize(),
+                $nameLength,
+                // extra field length              2 bytes
+                $extraLength + $padding
+            )
+        );
+        fwrite($outputStream, $this->getName());
+        if ($extraLength > 0) {
+            fwrite($outputStream, $extra);
+        }
+
+        if ($padding > 0) {
+            fwrite($outputStream, str_repeat(chr(0), $padding));
+        }
+
+        fseek($this->inputStream, $pos);
+        $data = unpack('vfileLength/vextraLength', fread($this->inputStream, 4));
+        fseek($this->inputStream, $data['fileLength'] + $data['extraLength'], SEEK_CUR);
+
+        $length = $this->getCompressedSize();
+        if ($this->getGeneralPurposeBitFlag(ZipEntry::GPBF_DATA_DESCRIPTOR)) {
+            $length += 12;
+            if ($this->isZip64ExtensionsRequired()) {
+                $length += 8;
+            }
+        }
+        stream_copy_to_stream($this->inputStream, $outputStream, $length);
+    }
+
+    function __destruct()
+    {
+        if ($this->entryContent !== null && is_resource($this->entryContent)) {
+            fclose($this->entryContent);
+        }
+    }
+
+}

+ 146 - 895
src/PhpZip/Model/ZipEntry.php

@@ -2,21 +2,17 @@
 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;
+use PhpZip\ZipFile;
 
 /**
- * This class is used to represent a ZIP file entry.
+ * 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
+interface ZipEntry
 {
     // Bit masks for initialized fields.
     const BIT_PLATFORM = 1,
@@ -31,405 +27,164 @@ class ZipEntry
 
     /** 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;
+    const METHOD_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;
+    const GPBF_UTF8 = 2048;
 
+    /** Local File Header signature. */
+    const LOCAL_FILE_HEADER_SIG = 0x04034B50;
+    /** Data Descriptor signature. */
+    const DATA_DESCRIPTOR_SIG = 0x08074B50;
     /**
-     * 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)
+     * The minimum length of the Local File Header record.
      *
-     * @var string
+     * 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
      */
-    private $name;
-
+    const LOCAL_FILE_HEADER_MIN_LEN = 30;
     /**
-     * Made by platform
-     *
-     * @var int
+     * 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
      */
-    private $platform;
+    const LOCAL_FILE_HEADER_FILE_NAME_LENGTH_POS = 26; // 1 << 11;
 
-    /**
-     * @var 2 bytes unsigned int
-     *
-     * @var int
-     */
-    private $general;
 
     /**
-     * Compression method
-     *
-     * @var int
+     * @return CentralDirectory
      */
-    private $method;
+    public function getCentralDirectory();
 
     /**
-     * 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
+     * @param CentralDirectory $centralDirectory
+     * @return ZipEntry
      */
-    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;
-    }
+    public function setCentralDirectory(CentralDirectory $centralDirectory);
 
     /**
      * Returns the ZIP entry name.
      *
      * @return string
      */
-    public function getName()
-    {
-        return $this->name;
-    }
+    public function getName();
 
     /**
      * Set entry name.
      *
-     * @see ZipEntry::__construct
-     * @see ZipOutputFile::rename()
-     *
      * @param string $name New entry name
+     * @return ZipEntry
      * @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;
-    }
+    public function setName($name);
 
     /**
-     * Get platform
-     *
-     * @return int
+     * @return int Get platform
      */
-    public function getPlatform()
-    {
-        return $this->isInit(self::BIT_PLATFORM) ? $this->platform & 0xffff : self::UNKNOWN;
-    }
+    public function getPlatform();
 
     /**
      * Set platform
      *
      * @param int $platform
+     * @return ZipEntry
      * @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);
-    }
+    public function setPlatform($platform);
 
     /**
      * 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
-                    )
-                )
-            );
-    }
+    public function getVersionNeededToExtract();
 
     /**
-     * @return int
+     * Set version needed to extract.
+     *
+     * @param int $version
+     * @return ZipEntry
      */
-    public function getRawMethod()
-    {
-        return $this->method & 0xff;
-    }
+    public function setVersionNeededToExtract($version);
 
     /**
      * @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();
-    }
+    public function isZip64ExtensionsRequired();
 
     /**
      * Returns the compressed size of this entry.
      *
      * @see int
      */
-    public function getCompressedSize()
-    {
-        return $this->compressedSize;
-    }
+    public function getCompressedSize();
 
     /**
      * Sets the compressed size of this entry.
      *
      * @param int $compressedSize The Compressed Size.
+     * @return ZipEntry
      * @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;
-    }
+    public function setCompressedSize($compressedSize);
 
     /**
      * Returns the uncompressed size of this entry.
      *
-     * @see #setCompressedSize
+     * @see ZipEntry::setCompressedSize
      */
-    public function getSize()
-    {
-        return $this->size;
-    }
+    public function getSize();
 
     /**
      * Sets the uncompressed size of this entry.
      *
      * @param int $size The (Uncompressed) Size.
+     * @return ZipEntry
      * @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;
-    }
+    public function setSize($size);
 
     /**
      * Return relative Offset Of Local File Header.
      *
      * @return int
      */
-    public function getOffset()
-    {
-        return $this->offset;
-    }
+    public function getOffset();
+
+    /**
+     * @param int $offset
+     * @return ZipEntry
+     * @throws ZipException
+     */
+    public function setOffset($offset);
 
     /**
      * Returns true if and only if this ZIP entry represents a directory entry
@@ -437,44 +192,23 @@ class ZipEntry
      *
      * @return bool
      */
-    public function isDirectory()
-    {
-        return $this->name[strlen($this->name) - 1] === '/';
-    }
+    public function isDirectory();
 
     /**
      * Returns the General Purpose Bit Flags.
      *
      * @return bool
      */
-    public function getGeneralPurposeBitFlags()
-    {
-        return $this->general & 0xffff;
-    }
+    public function getGeneralPurposeBitFlags();
 
     /**
      * Sets the General Purpose Bit Flags.
      *
      * @var int general
+     * @return ZipEntry
      * @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);
-    }
+    public function setGeneralPurposeBitFlags($general);
 
     /**
      * Returns the indexed General Purpose Bit Flag.
@@ -482,244 +216,86 @@ class ZipEntry
      * @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;
-    }
+    public function getGeneralPurposeBitFlag($mask);
 
     /**
-     * Sets the encryption flag for this ZIP entry.
+     * Sets the indexed General Purpose Bit Flag.
      *
-     * @param bool $encrypted
+     * @param int $mask
+     * @param bool $bit
+     * @return ZipEntry
      */
-    public function setEncrypted($encrypted)
-    {
-        $this->setGeneralPurposeBitFlag(self::GPBF_ENCRYPTED, $encrypted);
-    }
+    public function setGeneralPurposeBitFlag($mask, $bit);
 
     /**
-     * Sets the indexed General Purpose Bit Flag.
+     * Returns true if and only if this ZIP entry is encrypted.
      *
-     * @param int $mask
-     * @param bool $bit
+     * @return bool
      */
-    public function setGeneralPurposeBitFlag($mask, $bit)
-    {
-        if ($bit)
-            $this->general |= $mask;
-        else
-            $this->general &= ~$mask;
-    }
+    public function isEncrypted();
 
     /**
-     * Remove extra field from header id.
+     * Sets the encryption property to false and removes any other
+     * encryption artifacts.
      *
-     * @param int $headerId
-     * @return ExtraField|null
+     * @return ZipEntry
      */
-    public function removeExtraField($headerId)
-    {
-        return null !== $this->fields ? $this->fields->remove($headerId) : null;
-    }
+    public function clearEncryption();
 
     /**
-     * @param int $method
-     * @throws ZipException
+     * Sets the encryption flag for this ZIP entry.
+     *
+     * @param bool $encrypted
+     * @return ZipEntry
      */
-    public function setRawMethod($method)
-    {
-        if (0x0000 > $method || $method > 0xffff) {
-            throw new ZipException('method out of range');
-        }
-        $this->setMethod($method);
-    }
+    public function setEncrypted($encrypted);
 
     /**
      * Returns the compression method for this entry.
      *
      * @return int
      */
-    public function getMethod()
-    {
-        return $this->isInit(self::BIT_METHOD) ? $this->method & 0xffff : self::UNKNOWN;
-    }
+    public function getMethod();
 
     /**
      * Sets the compression method for this entry.
      *
      * @param int $method
+     * @return ZipEntry
      * @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)");
-        }
-    }
+    public function setMethod($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);
-    }
+    public function getTime();
 
     /**
      * Set time from unix timestamp.
      *
      * @param int $unixTimestamp
+     * @return ZipEntry
      */
-    public function setTime($unixTimestamp)
-    {
-        $known = self::UNKNOWN != $unixTimestamp;
-        if ($known) {
-            $this->dosTime = DateTimeConverter::toDosTime($unixTimestamp);
-        } else {
-            $this->dosTime = 0;
-        }
-        $this->setInit(self::BIT_DATE_TIME, $known);
-    }
-
-    /**
-     * @return 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);
-    }
+    public function setTime($unixTimestamp);
 
     /**
      * 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;
-    }
+    public function getExternalAttributes();
 
     /**
      * Sets the external file attributes.
      *
      * @param int $externalAttributes the external file attributes.
+     * @return ZipEntry
      * @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);
-    }
+    public function setExternalAttributes($externalAttributes);
 
     /**
      * Return extra field from header id.
@@ -727,21 +303,7 @@ class ZipEntry
      * @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);
-    }
+    public function getExtraField($headerId);
 
     /**
      * Add extra field.
@@ -750,92 +312,31 @@ class ZipEntry
      * @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);
-    }
+    public function addExtraField($field);
 
     /**
-     * Returns a protective copy of the serialized Extra Fields.
+     * Return exists extra field from header id.
      *
-     * @return string A new byte array holding the serialized Extra Fields.
-     *                null is never returned.
+     * @param int $headerId
+     * @return bool
      */
-    public function getExtra()
-    {
-        return $this->getExtraFields(false);
-    }
+    public function hasExtraField($headerId);
 
     /**
-     * @param bool $zip64
-     * @return bool|string
-     * @throws ZipException
+     * Remove extra field from header id.
+     *
+     * @param int $headerId
+     * @return ExtraField|null
      */
-    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();
-    }
+    public function removeExtraField($headerId);
 
     /**
-     * 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.
+     * Returns a protective copy of the serialized Extra Fields.
      *
-     * @return ExtraField|null
+     * @return string A new byte array holding the serialized Extra Fields.
+     *                null is never returned.
      */
-    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;
-    }
+    public function getExtra();
 
     /**
      * Sets the serialized Extra Fields by making a protective copy.
@@ -847,341 +348,91 @@ class ZipEntry
      *
      * @param string $data The byte array holding the serialized Extra Fields.
      * @throws ZipException if the serialized Extra Fields exceed 64 KB
+     * @return ZipEntry
      *         or do not conform to the ZIP File Format Specification
      */
-    public function setExtra($data)
-    {
-        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);
-    }
+    public function setExtra($data);
 
     /**
      * Returns comment entry
      *
      * @return string
      */
-    public function getComment()
-    {
-        return $this->comment;
-    }
+    public function getComment();
 
     /**
-     * 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.
+     * Set entry comment.
      *
-     * @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
+     * @param $comment
+     * @return ZipEntry
      */
-    public function setRawComment($comment)
-    {
-        $commentLength = strlen($comment);
-        if (0x0000 > $commentLength || $commentLength > 0xffff) {
-            throw new ZipException("Comment too long");
-        }
-        $this->comment = $comment;
-    }
+    public function setComment($comment);
 
     /**
      * @return bool
      */
-    public function isDataDescriptorRequired()
-    {
-        return self::UNKNOWN == ($this->getCrc() | $this->getCompressedSize() | $this->getSize());
-    }
+    public function isDataDescriptorRequired();
 
     /**
      * 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;
-    }
+    public function getCrc();
 
     /**
      * Set crc32 content.
      *
      * @param int $crc
+     * @return ZipEntry
      * @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);
-    }
+    public function setCrc($crc);
 
     /**
      * @return string
      */
-    public function getPassword()
-    {
-        return $this->password;
-    }
+    public function getPassword();
 
     /**
      * Set password and encryption method from entry
      *
      * @param string $password
      * @param null|int $encryptionMethod
+     * @return ZipEntry
      */
-    public function setPassword($password, $encryptionMethod = null)
-    {
-        $this->password = $password;
-        if ($encryptionMethod !== null) {
-            $this->setEncryptionMethod($encryptionMethod);
-        }
-        $this->setEncrypted(!empty($this->password));
-    }
+    public function setPassword($password, $encryptionMethod = null);
 
     /**
      * @return int
      */
-    public function getEncryptionMethod()
-    {
-        return $this->encryptionMethod;
-    }
+    public function getEncryptionMethod();
 
     /**
      * Set encryption method
      *
-     * @see ZipEntry::ENCRYPTION_METHOD_TRADITIONAL
-     * @see ZipEntry::ENCRYPTION_METHOD_WINZIP_AES
+     * @see ZipFile::ENCRYPTION_METHOD_TRADITIONAL
+     * @see ZipFile::ENCRYPTION_METHOD_WINZIP_AES
      *
      * @param int $encryptionMethod
+     * @return ZipEntry
+     * @throws ZipException
+     */
+    public function setEncryptionMethod($encryptionMethod);
+
+    /**
+     * Returns an string content of the given entry.
+     *
+     * @return null|string
      * @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);
-    }
+    public function getEntryContent();
 
+    /**
+     * Write local file header, encryption header, file data and data descriptor to output stream.
+     *
+     * @param resource $outputStream
+     * @throws ZipException
+     */
+    public function writeEntry($outputStream);
 }

+ 43 - 40
src/PhpZip/Model/ZipInfo.php

@@ -4,6 +4,7 @@ namespace PhpZip\Model;
 use PhpZip\Extra\NtfsExtraField;
 use PhpZip\Extra\WinZipAesEntryExtraField;
 use PhpZip\Util\FilesUtil;
+use PhpZip\ZipFile;
 
 /**
  * Zip info
@@ -85,7 +86,7 @@ class ZipInfo
     ];
 
     private static $valuesCompressionMethod = [
-        ZipEntry::METHOD_STORED => 'no compression',
+        ZipFile::METHOD_STORED => 'no compression',
         1 => 'shrink',
         2 => 'reduce level 1',
         3 => 'reduce level 2',
@@ -93,7 +94,7 @@ class ZipInfo
         5 => 'reduce level 4',
         6 => 'implode',
         7 => 'reserved for Tokenizing compression algorithm',
-        ZipEntry::METHOD_DEFLATED => 'deflate',
+        ZipFile::METHOD_DEFLATED => 'deflate',
         9 => 'deflate64',
         10 => 'PKWARE Data Compression Library Imploding (old IBM TERSE)',
         11 => 'reserved by PKWARE',
@@ -107,7 +108,7 @@ class ZipInfo
         19 => 'IBM LZ77 z Architecture (PFS)',
         97 => 'WavPack',
         98 => 'PPMd version I, Rev 1',
-        ZipEntry::WINZIP_AES => 'WinZip AES',
+        ZipEntry::METHOD_WINZIP_AES => 'WinZip AES',
     ];
 
     /**
@@ -214,34 +215,36 @@ class ZipInfo
         $this->platform = self::getPlatformName($entry);
         $this->version = $entry->getVersionNeededToExtract();
 
-        $attribs = str_repeat(" ", 12);
-        $xattr = (($entry->getRawExternalAttributes() >> 16) & 0xFFFF);
+        $attributes = str_repeat(" ", 12);
+        $externalAttributes = $entry->getExternalAttributes();
+        $xattr = (($externalAttributes >> 16) & 0xFFFF);
         switch ($entry->getPlatform()) {
             case self::MADE_BY_MS_DOS:
+            /** @noinspection PhpMissingBreakStatementInspection */
             case self::MADE_BY_WINDOWS_NTFS:
                 if ($entry->getPlatform() != self::MADE_BY_MS_DOS ||
                     ($xattr & 0700) !=
                     (0400 |
-                        (!($entry->getRawExternalAttributes() & 1) << 7) |
-                        (($entry->getRawExternalAttributes() & 0x10) << 2))
+                        (!($externalAttributes & 1) << 7) |
+                        (($externalAttributes & 0x10) << 2))
                 ) {
-                    $xattr = $entry->getRawExternalAttributes() & 0xFF;
-                    $attribs = ".r.-...     ";
-                    $attribs[2] = ($xattr & 0x01) ? '-' : 'w';
-                    $attribs[5] = ($xattr & 0x02) ? 'h' : '-';
-                    $attribs[6] = ($xattr & 0x04) ? 's' : '-';
-                    $attribs[4] = ($xattr & 0x20) ? 'a' : '-';
+                    $xattr = $externalAttributes & 0xFF;
+                    $attributes = ".r.-...     ";
+                    $attributes[2] = ($xattr & 0x01) ? '-' : 'w';
+                    $attributes[5] = ($xattr & 0x02) ? 'h' : '-';
+                    $attributes[6] = ($xattr & 0x04) ? 's' : '-';
+                    $attributes[4] = ($xattr & 0x20) ? 'a' : '-';
                     if ($xattr & 0x10) {
-                        $attribs[0] = 'd';
-                        $attribs[3] = 'x';
+                        $attributes[0] = 'd';
+                        $attributes[3] = 'x';
                     } else
-                        $attribs[0] = '-';
+                        $attributes[0] = '-';
                     if ($xattr & 0x08)
-                        $attribs[0] = 'V';
+                        $attributes[0] = 'V';
                     else {
                         $ext = strtolower(pathinfo($entry->getName(), PATHINFO_EXTENSION));
                         if (in_array($ext, ["com", "exe", "btm", "cmd", "bat"])) {
-                            $attribs[3] = 'x';
+                            $attributes[3] = 'x';
                         }
                     }
                     break;
@@ -250,51 +253,51 @@ class ZipInfo
             default: /* assume Unix-like */
                 switch ($xattr & self::UNX_IFMT) {
                     case self::UNX_IFDIR:
-                        $attribs[0] = 'd';
+                        $attributes[0] = 'd';
                         break;
                     case self::UNX_IFREG:
-                        $attribs[0] = '-';
+                        $attributes[0] = '-';
                         break;
                     case self::UNX_IFLNK:
-                        $attribs[0] = 'l';
+                        $attributes[0] = 'l';
                         break;
                     case self::UNX_IFBLK:
-                        $attribs[0] = 'b';
+                        $attributes[0] = 'b';
                         break;
                     case self::UNX_IFCHR:
-                        $attribs[0] = 'c';
+                        $attributes[0] = 'c';
                         break;
                     case self::UNX_IFIFO:
-                        $attribs[0] = 'p';
+                        $attributes[0] = 'p';
                         break;
                     case self::UNX_IFSOCK:
-                        $attribs[0] = 's';
+                        $attributes[0] = 's';
                         break;
                     default:
-                        $attribs[0] = '?';
+                        $attributes[0] = '?';
                         break;
                 }
-                $attribs[1] = ($xattr & self::UNX_IRUSR) ? 'r' : '-';
-                $attribs[4] = ($xattr & self::UNX_IRGRP) ? 'r' : '-';
-                $attribs[7] = ($xattr & self::UNX_IROTH) ? 'r' : '-';
-                $attribs[2] = ($xattr & self::UNX_IWUSR) ? 'w' : '-';
-                $attribs[5] = ($xattr & self::UNX_IWGRP) ? 'w' : '-';
-                $attribs[8] = ($xattr & self::UNX_IWOTH) ? 'w' : '-';
+                $attributes[1] = ($xattr & self::UNX_IRUSR) ? 'r' : '-';
+                $attributes[4] = ($xattr & self::UNX_IRGRP) ? 'r' : '-';
+                $attributes[7] = ($xattr & self::UNX_IROTH) ? 'r' : '-';
+                $attributes[2] = ($xattr & self::UNX_IWUSR) ? 'w' : '-';
+                $attributes[5] = ($xattr & self::UNX_IWGRP) ? 'w' : '-';
+                $attributes[8] = ($xattr & self::UNX_IWOTH) ? 'w' : '-';
 
                 if ($xattr & self::UNX_IXUSR)
-                    $attribs[3] = ($xattr & self::UNX_ISUID) ? 's' : 'x';
+                    $attributes[3] = ($xattr & self::UNX_ISUID) ? 's' : 'x';
                 else
-                    $attribs[3] = ($xattr & self::UNX_ISUID) ? 'S' : '-';  /* S==undefined */
+                    $attributes[3] = ($xattr & self::UNX_ISUID) ? 'S' : '-';  /* S==undefined */
                 if ($xattr & self::UNX_IXGRP)
-                    $attribs[6] = ($xattr & self::UNX_ISGID) ? 's' : 'x';  /* == UNX_ENFMT */
+                    $attributes[6] = ($xattr & self::UNX_ISGID) ? 's' : 'x';  /* == UNX_ENFMT */
                 else
-                    $attribs[6] = ($xattr & self::UNX_ISGID) ? 'S' : '-';  /* SunOS 4.1.x */
+                    $attributes[6] = ($xattr & self::UNX_ISGID) ? 'S' : '-';  /* SunOS 4.1.x */
                 if ($xattr & self::UNX_IXOTH)
-                    $attribs[9] = ($xattr & self::UNX_ISVTX) ? 't' : 'x';  /* "sticky bit" */
+                    $attributes[9] = ($xattr & self::UNX_ISVTX) ? 't' : 'x';  /* "sticky bit" */
                 else
-                    $attribs[9] = ($xattr & self::UNX_ISVTX) ? 'T' : '-';  /* T==undefined */
+                    $attributes[9] = ($xattr & self::UNX_ISVTX) ? 'T' : '-';  /* T==undefined */
         }
-        $this->attributes = trim($attribs);
+        $this->attributes = trim($attributes);
     }
 
     /**
@@ -305,7 +308,7 @@ class ZipInfo
     {
         $return = '';
         if ($entry->isEncrypted()) {
-            if ($entry->getMethod() === ZipEntry::WINZIP_AES) {
+            if ($entry->getMethod() === ZipEntry::METHOD_WINZIP_AES) {
                 $field = $entry->getExtraField(WinZipAesEntryExtraField::getHeaderId());
                 $return = ucfirst(self::$valuesCompressionMethod[$entry->getMethod()]);
                 if ($field !== null) {

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

@@ -1,22 +0,0 @@
-<?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 '';
-    }
-}

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

@@ -1,46 +0,0 @@
-<?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();
-}

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

@@ -1,54 +0,0 @@
-<?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;
-        }
-    }
-}

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

@@ -1,46 +0,0 @@
-<?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;
-    }
-}

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

@@ -1,56 +0,0 @@
-<?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);
-    }
-}

+ 3 - 2
src/PhpZip/Util/CryptoUtil.php

@@ -1,6 +1,7 @@
 <?php
 namespace PhpZip\Util;
 
+use PhpZip\Exception\RuntimeException;
 use PhpZip\Exception\ZipException;
 
 /**
@@ -14,7 +15,7 @@ class CryptoUtil
      *
      * @param int $length
      * @return string
-     * @throws ZipException
+     * @throws RuntimeException
      */
     public static final function randomBytes($length)
     {
@@ -26,7 +27,7 @@ class CryptoUtil
         } elseif (function_exists('mcrypt_create_iv')) {
             return mcrypt_create_iv($length);
         } else {
-            throw new ZipException('Extension openssl or mcrypt not loaded');
+            throw new RuntimeException('Extension openssl or mcrypt not loaded');
         }
     }
 }

+ 0 - 115
src/PhpZip/ZipConstants.php

@@ -1,115 +0,0 @@
-<?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;
-
-}

Fichier diff supprimé car celui-ci est trop grand
+ 684 - 545
src/PhpZip/ZipFile.php


+ 0 - 1475
src/PhpZip/ZipOutputFile.php

@@ -1,1475 +0,0 @@
-<?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;
-
-    /**
-     * ZipAlign setting
-     *
-     * @var int
-     */
-    private $align;
-
-    /**
-     * 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);
-    }
-
-    /**
-     * Open zip file from update.
-     *
-     * @param string $filename
-     * @return ZipOutputFile
-     * @throws IllegalArgumentException
-     * @see ZipOutputFile::__construct()
-     */
-    public static function openFromFile($filename)
-    {
-        if (empty($filename)) {
-            throw new IllegalArgumentException("Zip file is null");
-        }
-        return new self(ZipFile::openFromFile($filename));
-    }
-
-    /**
-     * Count zip entries.
-     *
-     * @return int
-     */
-    public function count()
-    {
-        return sizeof($this->entries);
-    }
-
-    /**
-     * 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 && strlen($comment) !== 0) {
-            $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 || strlen($data) === 0) {
-            throw new IllegalArgumentException("Data is empty");
-        }
-        if ($entryName === null || strlen($entryName) === 0) {
-            throw new IllegalArgumentException("Incorrect entry name " . $entryName);
-        }
-        $this->validateCompressionMethod($compressionMethod);
-
-        $externalAttributes = 0100644 << 16;
-
-        $entry = new ZipEntry($entryName);
-        $entry->setMethod($compressionMethod);
-        $entry->setTime(time());
-        $entry->setExternalAttributes($externalAttributes);
-
-        $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 ($inputDir === null || strlen($inputDir) === 0) {
-            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, '/');
-            if (is_dir($file)) {
-                FilesUtil::isEmptyDir($file) && $this->addEmptyDir($filename);
-            } elseif (is_file($file)) {
-                $this->addFromFile($file, $filename, $compressionMethod);
-            }
-        }
-        return $this->count() > $count;
-    }
-
-    /**
-     * Add an empty directory in the zip archive.
-     *
-     * @param string $dirName
-     * @throws IllegalArgumentException
-     */
-    public function addEmptyDir($dirName)
-    {
-        $dirName = (string)$dirName;
-        if (strlen($dirName) === 0) {
-            throw new IllegalArgumentException("dirName null or not string");
-        }
-        $dirName = rtrim($dirName, '/') . '/';
-        if (!isset($this->entries[$dirName])) {
-            $externalAttributes = 040755 << 16;
-
-            $entry = new ZipEntry($dirName);
-            $entry->setTime(time());
-            $entry->setMethod(ZipEntry::METHOD_STORED);
-            $entry->setSize(0);
-            $entry->setCompressedSize(0);
-            $entry->setCrc(0);
-            $entry->setExternalAttributes($externalAttributes);
-
-            $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 (strlen($entryName) === 0) {
-            throw new IllegalArgumentException("Incorrect entry name " . $entryName);
-        }
-        $this->validateCompressionMethod($compressionMethod);
-
-        $fstat = fstat($stream);
-        $mode = sprintf('%o', $fstat['mode']);
-        $externalAttributes = (octdec($mode) & 0xffff) << 16;
-
-        $entry = new ZipEntry($entryName);
-        $entry->setMethod($compressionMethod);
-        $entry->setTime(time());
-        $entry->setExternalAttributes($externalAttributes);
-
-        $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 || strlen($globPattern) === 0) {
-            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, '/');
-            if (is_dir($file)) {
-                FilesUtil::isEmptyDir($file) && $this->addEmptyDir($filename);
-            } elseif (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, '/');
-            if (is_dir($file)) {
-                FilesUtil::isEmptyDir($file) && $this->addEmptyDir($filename);
-            } elseif (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;
-    }
-
-    /**
-     * @param int|null $align
-     */
-    public function setZipAlign($align = 4)
-    {
-        if ($align === null) {
-            $this->align = null;
-            return;
-        }
-        $this->align = (int)$align;
-    }
-
-    /**
-     * 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::PLATFORM_UNIX);
-        }
-        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);
-
-        // Commit changes.
-        $entry->setGeneralPurposeBitFlags($general);
-        $entry->setRawOffset($offset);
-
-        // 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();
-
-        // zip align
-        $padding = 0;
-        if ($this->align !== null && !$entry->isEncrypted() && $entry->getMethod() === ZipEntry::METHOD_STORED) {
-            $padding =
-                (
-                    $this->align -
-                    (
-                        $offset +
-                        self::LOCAL_FILE_HEADER_MIN_LEN +
-                        strlen($entry->getName()) +
-                        strlen($extra)
-                    ) % $this->align
-                ) % $this->align;
-        }
-
-        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) + $padding
-        ));
-        // file name (variable size)
-        fwrite($outputHandle, $entry->getName());
-        // extra field (variable size)
-        fwrite($outputHandle, $extra);
-
-        if ($padding > 0) {
-            fwrite($outputHandle, str_repeat(chr(0), $padding));
-        }
-
-        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 (strlen($outputFilename) === 0) {
-            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)
-    {
-        $entryName = (string)$entryName;
-        if (strlen($entryName) === 0) {
-            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);
-    }
-}

+ 1378 - 0
tests/PhpZip/ZipFileTest.php

@@ -0,0 +1,1378 @@
+<?php
+namespace PhpZip;
+
+use PhpZip\Exception\ZipAuthenticationException;
+use PhpZip\Model\ZipEntry;
+use PhpZip\Util\CryptoUtil;
+use PhpZip\Util\FilesUtil;
+
+/**
+ * ZipFile test
+ */
+class ZipFileTest 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);
+        }
+    }
+
+    /**
+     * Test create, open and extract empty archive.
+     */
+    public function testEmptyArchive()
+    {
+        $zipFile = new ZipFile();
+        $zipFile->saveAsFile($this->outputFilename);
+        $zipFile->close();
+
+        self::assertCorrectEmptyZip($this->outputFilename);
+
+        $extractPath = sys_get_temp_dir() . '/extract-empty-dir';
+        if (is_dir($extractPath)) {
+            FilesUtil::removeDir($extractPath);
+        }
+        self::assertTrue(mkdir($extractPath, 0755, true));
+
+        $zipFile->openFile($this->outputFilename);
+        self::assertEquals(count($zipFile), 0);
+        self::assertEquals($zipFile->count(), 0);
+        $zipFile->extractTo($extractPath);
+        $zipFile->close();
+
+        self::assertTrue(FilesUtil::isEmptyDir($extractPath));
+        FilesUtil::removeDir($extractPath);
+    }
+
+    /**
+     * @expectedException \PhpZip\Exception\InvalidArgumentException
+     * @expectedExceptionMessage can't exists
+     */
+    public function testCantExistsOpenFile()
+    {
+        $zipFile = new ZipFile();
+        $zipFile->openFile(uniqid());
+    }
+
+    /**
+     * @expectedException \PhpZip\Exception\ZipException
+     * @expectedExceptionMessage can't open
+     */
+    public function testCantOpenFile()
+    {
+        self::assertNotFalse(file_put_contents($this->outputFilename, 'content'));
+        self::assertTrue(chmod($this->outputFilename, 0222));
+
+        $zipFile = new ZipFile();
+        $zipFile->openFile($this->outputFilename);
+    }
+
+    /**
+     * @expectedException \PhpZip\Exception\InvalidArgumentException
+     * @expectedExceptionMessage Data not available
+     */
+    public function testOpenFromStringNull()
+    {
+        $zipFile = new ZipFile();
+        $zipFile->openFromString(null);
+    }
+
+    /**
+     * @expectedException \PhpZip\Exception\InvalidArgumentException
+     * @expectedExceptionMessage Data not available
+     */
+    public function testOpenFromStringEmpty()
+    {
+        $zipFile = new ZipFile();
+        $zipFile->openFromString("");
+    }
+
+    public function testOpenFromString()
+    {
+        $zipFile = new ZipFile();
+        $zipFile->addFromString('file', 'content', ZipFile::METHOD_DEFLATED);
+        $zipContents = $zipFile->outputAsString();
+        $zipFile->close();
+
+        $zipFile->openFromString($zipContents);
+        self::assertTrue(isset($zipFile['file']));
+        self::assertEquals($zipFile['file'], 'content');
+        $zipFile->close();
+    }
+
+    public function testOpenFromStream()
+    {
+        $zipFile = new ZipFile();
+        $zipFile->addFromString('file', 'content', ZipFile::METHOD_DEFLATED)
+            ->saveAsFile($this->outputFilename);
+        $zipFile->close();
+
+        $handle = fopen($this->outputFilename, 'rb');
+        $zipFile->openFromStream($handle);
+        self::assertTrue(isset($zipFile['file']));
+        self::assertEquals($zipFile['file'], 'content');
+        $zipFile->close();
+    }
+
+    /**
+     * @expectedException \PhpZip\Exception\InvalidArgumentException
+     * @expectedExceptionMessage Invalid stream resource
+     */
+    public function testOpenFromInvalidStream()
+    {
+        $zipFile = new ZipFile();
+        $zipFile->openFromStream("stream resource");
+    }
+
+    /**
+     * @expectedException \PhpZip\Exception\InvalidArgumentException
+     * @expectedExceptionMessage Resource cannot seekable stream.
+     */
+    public function testOpenFromStreamNoSeekable()
+    {
+        if (!$fp = @fopen("http://localhost", 'r')) {
+            if (!$fp = @fopen("http://example.org", 'r')) {
+                $this->markTestSkipped('not connected to localhost or remote host');
+                return;
+            }
+        }
+
+        $zipFile = new ZipFile();
+        $zipFile->openFromStream($fp);
+    }
+
+    /**
+     * No modified archive
+     *
+     * @see ZipOutputFile::create()
+     */
+    public function testNoModifiedArchive()
+    {
+        $zipFile = new ZipFile();
+        $zipFile->addDir(__DIR__);
+        $zipFile->saveAsFile($this->outputFilename);
+        $zipFile->close();
+
+        self::assertCorrectZipArchive($this->outputFilename);
+
+        $zipFile->openFile($this->outputFilename);
+        $zipFile->saveAsFile($this->outputFilename);
+        $zipFile->close();
+
+        self::assertCorrectZipArchive($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__);
+        $outputFromString2 = file_get_contents(dirname(dirname(__DIR__)) . DIRECTORY_SEPARATOR . 'README.md');
+        $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__);
+        $filenameFromString2 = 'test_file.txt';
+        $filenameFromFile = 'data/test file.txt';
+        $filenameFromStream = 'data/ডিরেক্টরি/αρχείο.json';
+        $emptyDirName = 'empty dir/пустой каталог/空目錄/ไดเรกทอรีที่ว่างเปล่า/';
+        $emptyDirName2 = 'empty dir/пустой каталог/';
+        $emptyDirName3 = 'empty dir/пустой каталог/ещё один пустой каталог/';
+
+        $tempFile = tempnam(sys_get_temp_dir(), 'txt');
+        file_put_contents($tempFile, $outputFromFile);
+
+        $tempStream = tmpfile();
+        fwrite($tempStream, $outputFromStream);
+
+        $zipFile = new ZipFile;
+        $zipFile->addFromString($filenameFromString, $outputFromString);
+        $zipFile->addFile($tempFile, $filenameFromFile);
+        $zipFile->addFromStream($tempStream, $filenameFromStream);
+        $zipFile->addEmptyDir($emptyDirName);
+        $zipFile[$filenameFromString2] = $outputFromString2;
+        $zipFile[$emptyDirName2] = null;
+        $zipFile[$emptyDirName3] = 'this content ignoring';
+        $zipFile->saveAsFile($this->outputFilename);
+        $zipFile->close();
+        unlink($tempFile);
+
+        self::assertCorrectZipArchive($this->outputFilename);
+
+        $zipFile->openFile($this->outputFilename);
+        self::assertEquals(count($zipFile), 7);
+        self::assertEquals($zipFile[$filenameFromString], $outputFromString);
+        self::assertEquals($zipFile[$filenameFromFile], $outputFromFile);
+        self::assertEquals($zipFile[$filenameFromStream], $outputFromStream);
+        self::assertEquals($zipFile[$filenameFromString2], $outputFromString2);
+        self::assertTrue(isset($zipFile[$emptyDirName]));
+        self::assertTrue(isset($zipFile[$emptyDirName2]));
+        self::assertTrue(isset($zipFile[$emptyDirName3]));
+        self::assertTrue($zipFile->isDirectory($emptyDirName));
+        self::assertTrue($zipFile->isDirectory($emptyDirName2));
+        self::assertTrue($zipFile->isDirectory($emptyDirName3));
+
+        $listFiles = $zipFile->getListFiles();
+        self::assertEquals($listFiles[0], $filenameFromString);
+        self::assertEquals($listFiles[1], $filenameFromFile);
+        self::assertEquals($listFiles[2], $filenameFromStream);
+        self::assertEquals($listFiles[3], $emptyDirName);
+        self::assertEquals($listFiles[4], $filenameFromString2);
+        self::assertEquals($listFiles[5], $emptyDirName2);
+        self::assertEquals($listFiles[6], $emptyDirName3);
+
+        $zipFile->close();
+    }
+
+    /**
+     * Test compression method from image file.
+     */
+    public function testCompressionMethodFromImageMimeType()
+    {
+        if (!function_exists('mime_content_type')) {
+            $this->markTestSkipped('Function mime_content_type not exists');
+        }
+        $outputFilename = $this->outputFilename;
+        $this->outputFilename .= '.gif';
+        self::assertNotFalse(
+            file_put_contents(
+                $this->outputFilename,
+                base64_decode('R0lGODlhAQABAJAAAP8AAAAAACH5BAUQAAAALAAAAAABAAEAAAICBAEAOw==')
+            )
+        );
+        $basename = basename($this->outputFilename);
+
+        $zipFile = new ZipFile();
+        $zipFile->addFile($this->outputFilename, $basename);
+        $zipFile->saveAsFile($outputFilename);
+        unlink($this->outputFilename);
+        $this->outputFilename = $outputFilename;
+
+        $zipFile->openFile($this->outputFilename);
+        $info = $zipFile->getEntryInfo($basename);
+        self::assertEquals($info->getMethod(), 'No compression');
+        $zipFile->close();
+    }
+
+    /**
+     * Create archive and add directory recursively.
+     */
+    public function testAddDirRecursively()
+    {
+        $inputDir = dirname(dirname(__DIR__)) . DIRECTORY_SEPARATOR . "src";
+
+        $zipFile = new ZipFile();
+        $zipFile->addDir($inputDir);
+        $zipFile->saveAsFile($this->outputFilename);
+        $zipFile->close();
+
+        self::assertCorrectZipArchive($this->outputFilename);
+    }
+
+    /**
+     * Create archive and add directory not recursively.
+     */
+    public function testAddDirNotRecursively()
+    {
+        $inputDir = dirname(dirname(__DIR__));
+        $recursive = false;
+
+        $zipFile = new ZipFile();
+        $zipFile->addDir($inputDir, $recursive);
+        $zipFile->saveAsFile($this->outputFilename);
+        $zipFile->close();
+
+        self::assertCorrectZipArchive($this->outputFilename);
+    }
+
+    /**
+     * Create archive and add directory and put files to path.
+     */
+    public function testAddDirAndMoveToPath()
+    {
+        $inputDir = __DIR__;
+        $files = array_slice(scandir($inputDir), 2);
+
+        $recursive = true;
+
+        $zipFile = new ZipFile();
+        $moveToPath = 'Library/tests';
+        $zipFile->addDir($inputDir, $recursive, $moveToPath);
+        $zipFile->saveAsFile($this->outputFilename);
+        $zipFile->close();
+
+        self::assertCorrectZipArchive($this->outputFilename);
+
+        $zipFile->openFile($this->outputFilename);
+        self::assertEquals(count($zipFile), count($files));
+        foreach ($files as $file) {
+            self::assertTrue(isset($zipFile[$moveToPath . '/' . $file]));
+            self::assertEquals($zipFile[$moveToPath . '/' . $file], file_get_contents($inputDir . '/' . $file));
+        }
+        $zipFile->close();
+    }
+
+    /**
+     * Create archive and add directory with ignore files list.
+     */
+    public function testAddDirAndIgnoreFiles()
+    {
+        $inputDir = dirname(dirname(__DIR__)) . DIRECTORY_SEPARATOR;
+
+        $recursive = false;
+
+        $zipFile = new ZipFile();
+        $ignoreFiles = ['tests/', '.git/', 'composer.lock', 'vendor/', ".idea/"];
+        $moveToPath = 'PhpZip Library';
+        $zipFile->addDir($inputDir, $recursive, $moveToPath, $ignoreFiles);
+        $zipFile->saveAsFile($this->outputFilename);
+        $zipFile->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;
+
+        $zipFile = new ZipFile();
+        $ignoreFiles = ['tests/', '.git/', 'composer.lock', 'vendor/', ".idea/copyright/"];
+        $moveToPath = 'PhpZip Library';
+        $zipFile->addDir($inputDir, $recursive, $moveToPath, $ignoreFiles);
+        $zipFile->saveAsFile($this->outputFilename);
+        $zipFile->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;
+
+        $zipFile = new ZipFile();
+        $zipFile->addFilesFromGlob($inputDir, '**.{php,xml}', $moveToPath, $recursive);
+        $zipFile->saveAsFile($this->outputFilename);
+        $zipFile->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;
+
+        $zipFile = new ZipFile();
+        $zipFile->addFilesFromGlob($inputDir, '**.{php,xml}', $moveToPath, $recursive);
+        $zipFile->saveAsFile($this->outputFilename);
+        $zipFile->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;
+
+        $zipFile = new ZipFile();
+        $zipFile->addFilesFromRegex($inputDir, '~\.(xml|php)$~i', $moveToPath, $recursive);
+        $zipFile->saveAsFile($this->outputFilename);
+        $zipFile->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;
+
+        $zipFile = new ZipFile();
+        $zipFile->addFilesFromRegex($inputDir, '~\.(xml|php)$~i', $recursive, $moveToPath);
+        $zipFile->saveAsFile($this->outputFilename);
+        $zipFile->close();
+
+        self::assertCorrectZipArchive($this->outputFilename);
+    }
+
+    /**
+     * Rename zip entry name.
+     */
+    public function testRename()
+    {
+        $oldName = basename(__FILE__);
+        $newName = 'tests/' . $oldName;
+
+        $zipFile = new ZipFile();
+        $zipFile->addDir(__DIR__);
+        $zipFile->saveAsFile($this->outputFilename);
+        $zipFile->close();
+
+        self::assertCorrectZipArchive($this->outputFilename);
+
+        $zipFile->openFile($this->outputFilename);
+        $zipFile->rename($oldName, $newName);
+        $zipFile->addFromString('file1.txt', 'content');
+        $zipFile->rename('file1.txt', 'file2.txt');
+        $zipFile->saveAsFile($this->outputFilename);
+        $zipFile->close();
+
+        self::assertCorrectZipArchive($this->outputFilename);
+
+        $zipFile->openFile($this->outputFilename);
+        self::assertFalse(isset($zipFile[$oldName]));
+        self::assertTrue(isset($zipFile[$newName]));
+        self::assertFalse(isset($zipFile['file1.txt']));
+        self::assertTrue(isset($zipFile['file2.txt']));
+        $zipFile->close();
+    }
+
+    /**
+     * Delete entry from name.
+     */
+    public function testDeleteFromName()
+    {
+        $inputDir = dirname(dirname(__DIR__)) . DIRECTORY_SEPARATOR;
+        $deleteEntryName = 'composer.json';
+
+        $zipFile = new ZipFile();
+        $zipFile->addDir($inputDir, false);
+        $zipFile->saveAsFile($this->outputFilename);
+        $zipFile->close();
+
+        self::assertCorrectZipArchive($this->outputFilename);
+
+        $zipFile->openFile($this->outputFilename);
+        $zipFile->deleteFromName($deleteEntryName);
+        $zipFile->saveAsFile($this->outputFilename);
+        $zipFile->close();
+
+        self::assertCorrectZipArchive($this->outputFilename);
+
+        $zipFile->openFile($this->outputFilename);
+        self::assertFalse(isset($zipFile[$deleteEntryName]));
+        $zipFile->close();
+    }
+
+    /**
+     * Delete zip entries from glob pattern
+     */
+    public function testDeleteFromGlob()
+    {
+        $inputDir = dirname(dirname(__DIR__));
+
+        $zipFile = new ZipFile();
+        $zipFile->addFilesFromGlob($inputDir, '**.{php,xml,json}', '/', true);
+        $zipFile->saveAsFile($this->outputFilename);
+        $zipFile->close();
+
+        self::assertCorrectZipArchive($this->outputFilename);
+
+        $zipFile->openFile($this->outputFilename);
+        $zipFile->deleteFromGlob('**.{xml,json}');
+        $zipFile->saveAsFile($this->outputFilename);
+        $zipFile->close();
+
+        self::assertCorrectZipArchive($this->outputFilename);
+
+        $zipFile->openFile($this->outputFilename);
+        self::assertFalse(isset($zipFile['composer.json']));
+        self::assertFalse(isset($zipFile['bootstrap.xml']));
+        $zipFile->close();
+    }
+
+    /**
+     * Delete entries from regex pattern
+     */
+    public function testDeleteFromRegex()
+    {
+        $inputDir = dirname(dirname(__DIR__));
+
+        $zipFile = new ZipFile();
+        $zipFile->addFilesFromRegex($inputDir, '~\.(xml|php|json)$~i', 'Path', true);
+        $zipFile->saveAsFile($this->outputFilename);
+        $zipFile->close();
+
+        self::assertCorrectZipArchive($this->outputFilename);
+
+        $zipFile->openFile($this->outputFilename);
+        $zipFile->deleteFromRegex('~\.(json)$~i');
+        $zipFile->saveAsFile($this->outputFilename);
+        $zipFile->close();
+
+        self::assertCorrectZipArchive($this->outputFilename);
+
+        $zipFile->openFile($this->outputFilename);
+        self::assertFalse(isset($zipFile['Path/composer.json']));
+        self::assertTrue(isset($zipFile['Path/bootstrap.xml']));
+        $zipFile->close();
+    }
+
+    /**
+     * Delete all entries
+     */
+    public function testDeleteAll()
+    {
+        $zipFile = new ZipFile();
+        $zipFile->addDir(__DIR__);
+        $zipFile->saveAsFile($this->outputFilename);
+        $zipFile->close();
+
+        self::assertCorrectZipArchive($this->outputFilename);
+
+        $zipFile->openFile($this->outputFilename);
+        self::assertTrue($zipFile->count() > 0);
+        $zipFile->deleteAll();
+        $zipFile->saveAsFile($this->outputFilename);
+        $zipFile->close();
+
+        self::assertCorrectEmptyZip($this->outputFilename);
+
+        $zipFile->openFile($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
+            . "ความคิดเห็นนี้ไฟล์ซิป";
+
+        $zipFile = new ZipFile();
+        $zipFile->setArchiveComment($comment);
+        $zipFile->addFile(__FILE__);
+        $zipFile->saveAsFile($this->outputFilename);
+        $zipFile->close();
+
+        self::assertCorrectZipArchive($this->outputFilename);
+
+        $zipFile->openFile($this->outputFilename);
+        self::assertEquals($zipFile->getArchiveComment(), $comment);
+        $zipFile->setArchiveComment(null); // remove archive comment
+        $zipFile->saveAsFile($this->outputFilename);
+        $zipFile->close();
+
+        self::assertCorrectZipArchive($this->outputFilename);
+
+        // check empty comment
+        $zipFile->openFile($this->outputFilename);
+        self::assertEquals($zipFile->getArchiveComment(), "");
+        $zipFile->close();
+    }
+
+    /**
+     * Test very long archive comment.
+     *
+     * @expectedException \PhpZip\Exception\InvalidArgumentException
+     */
+    public function testVeryLongArchiveComment()
+    {
+        $comment = "Very long comment" . PHP_EOL .
+            "Очень длинный комментарий" . PHP_EOL;
+        $comment = str_repeat($comment, ceil(0xffff / strlen($comment)) + strlen($comment) + 1);
+
+        $zipFile = new ZipFile();
+        $zipFile->setArchiveComment($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 - 😀 ⛈ ❤️ 🤴🏽"
+            ],
+        ];
+
+        // create archive with entry comments
+        $zipFile = new ZipFile();
+        foreach ($entries as $entryName => $item) {
+            $zipFile->addFromString($entryName, $item['data']);
+            $zipFile->setEntryComment($entryName, $item['comment']);
+        }
+        $zipFile->saveAsFile($this->outputFilename);
+        $zipFile->close();
+
+        self::assertCorrectZipArchive($this->outputFilename);
+
+        // check and modify comments
+        $zipFile->openFile($this->outputFilename);
+        foreach ($zipFile->getListFiles() as $entryName) {
+            $entriesItem = $entries[$entryName];
+            self::assertNotEmpty($entriesItem);
+            self::assertEquals($zipFile[$entryName], $entriesItem['data']);
+            self::assertEquals($zipFile->getEntryComment($entryName), (string)$entriesItem['comment']);
+        }
+        // modify comment
+        $entries['file5.txt']['comment'] = mt_rand(1, 100000000);
+        $zipFile->setEntryComment('file5.txt', $entries['file5.txt']['comment']);
+        $zipFile->saveAsFile($this->outputFilename);
+        $zipFile->close();
+
+        self::assertCorrectZipArchive($this->outputFilename);
+
+        // check modify comments
+        $zipFile->openFile($this->outputFilename);
+        foreach ($entries as $entryName => $entriesItem) {
+            self::assertTrue(isset($zipFile[$entryName]));
+            self::assertEquals($zipFile->getEntryComment($entryName), (string)$entriesItem['comment']);
+            self::assertEquals($zipFile[$entryName], $entriesItem['data']);
+        }
+        $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);
+
+        $zipFile = new ZipFile();
+        $zipFile->addFile(__FILE__, 'test');
+        $zipFile->setEntryComment('test', $comment);
+    }
+
+    /**
+     * Test all available support compression methods.
+     */
+    public function testCompressionMethod()
+    {
+        $entries = [
+            '1' => [
+                'data' => CryptoUtil::randomBytes(255),
+                'method' => ZipFile::METHOD_STORED,
+                'expected' => 'No compression',
+            ],
+            '2' => [
+                'data' => CryptoUtil::randomBytes(255),
+                'method' => ZipFile::METHOD_DEFLATED,
+                'expected' => 'Deflate',
+            ],
+        ];
+        if (extension_loaded("bz2")) {
+            $entries['3'] = [
+                'data' => CryptoUtil::randomBytes(255),
+                'method' => ZipFile::METHOD_BZIP2,
+                'expected' => 'Bzip2',
+            ];
+        }
+
+        $zipFile = new ZipFile();
+        foreach ($entries as $entryName => $item) {
+            $zipFile->addFromString($entryName, $item['data'], $item['method']);
+        }
+        $zipFile->saveAsFile($this->outputFilename);
+        $zipFile->close();
+
+        self::assertCorrectZipArchive($this->outputFilename);
+
+        $zipFile->openFile($this->outputFilename);
+        $zipFile->setCompressionLevel(ZipFile::LEVEL_BEST_COMPRESSION);
+        $zipAllInfo = $zipFile->getAllInfo();
+
+        foreach ($zipAllInfo as $entryName => $info) {
+            self::assertEquals($zipFile[$entryName], $entries[$entryName]['data']);
+            self::assertEquals($info->getMethod(), $entries[$entryName]['expected']);
+            $entryInfo = $zipFile->getEntryInfo($entryName);
+            self::assertEquals($entryInfo, $info);
+        }
+        $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,
+        ];
+
+        $extractPath = sys_get_temp_dir() . '/zipExtract' . uniqid();
+        if (!is_dir($extractPath)) {
+            mkdir($extractPath, 0755, true);
+        }
+
+        $zipFile = new ZipFile();
+        foreach ($entries as $entryName => $value) {
+            if ($value === null) {
+                $zipFile->addEmptyDir($entryName);
+            } else {
+                $zipFile->addFromString($entryName, $value);
+            }
+        }
+        $zipFile->saveAsFile($this->outputFilename);
+        $zipFile->close();
+
+        $zipFile->openFile($this->outputFilename);
+        $zipFile->extractTo($extractPath);
+        foreach ($entries as $entryName => $value) {
+            $fullExtractedFilename = $extractPath . DIRECTORY_SEPARATOR . $entryName;
+            if ($value === null) {
+                self::assertTrue(is_dir($fullExtractedFilename));
+                self::assertTrue(FilesUtil::isEmptyDir($fullExtractedFilename));
+            } else {
+                self::assertTrue(is_file($fullExtractedFilename));
+                $contents = file_get_contents($fullExtractedFilename);
+                self::assertEquals($contents, $value);
+            }
+        }
+        $zipFile->close();
+
+        FilesUtil::removeDir($extractPath);
+    }
+
+    /**
+     * 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/'
+        ];
+
+        $extractPath = sys_get_temp_dir() . '/zipExtractTest';
+        if (is_dir($extractPath)) {
+            FilesUtil::removeDir($extractPath);
+        }
+        self::assertTrue(mkdir($extractPath, 0755, true));
+
+        $zipFile = new ZipFile();
+        $zipFile->addAll($entries);
+        $zipFile->saveAsFile($this->outputFilename);
+        $zipFile->close();
+
+        $zipFile->openFile($this->outputFilename);
+        $zipFile->extractTo($extractPath, $extractEntries);
+
+        foreach ($entries as $entryName => $value) {
+            $fullExtractFilename = $extractPath . DIRECTORY_SEPARATOR . $entryName;
+            if (in_array($entryName, $extractEntries)) {
+                if ($value === null) {
+                    self::assertTrue(is_dir($fullExtractFilename));
+                    self::assertTrue(FilesUtil::isEmptyDir($fullExtractFilename));
+                } else {
+                    self::assertTrue(is_file($fullExtractFilename));
+                    $contents = file_get_contents($fullExtractFilename);
+                    self::assertEquals($contents, $value);
+                }
+            } else {
+                if ($value === null) {
+                    self::assertFalse(is_dir($fullExtractFilename));
+                } else {
+                    self::assertFalse(is_file($fullExtractFilename));
+                }
+            }
+        }
+        self::assertFalse(is_file($extractPath . DIRECTORY_SEPARATOR . 'test/test/test.txt'));
+        $zipFile->extractTo($extractPath, 'test/test/test.txt');
+        self::assertTrue(is_file($extractPath . DIRECTORY_SEPARATOR . 'test/test/test.txt'));
+
+        $zipFile->close();
+        FilesUtil::removeDir($extractPath);
+    }
+
+    /**
+     * @expectedException \PhpZip\Exception\ZipException
+     * @expectedExceptionMessage not found
+     */
+    public function testExtractFail()
+    {
+        $zipFile = new ZipFile();
+        $zipFile['file'] = 'content';
+        $zipFile->saveAsFile($this->outputFilename);
+        $zipFile->close();
+
+        $zipFile->openFile($this->outputFilename);
+        $zipFile->extractTo('path/to/path');
+    }
+
+    /**
+     * @expectedException \PhpZip\Exception\ZipException
+     * @expectedExceptionMessage Destination is not directory
+     */
+    public function testExtractFail2()
+    {
+        $zipFile = new ZipFile();
+        $zipFile['file'] = 'content';
+        $zipFile->saveAsFile($this->outputFilename);
+        $zipFile->close();
+
+        $zipFile->openFile($this->outputFilename);
+        $zipFile->extractTo($this->outputFilename);
+    }
+
+    /**
+     * @expectedException \PhpZip\Exception\ZipException
+     * @expectedExceptionMessage Destination is not writable directory
+     */
+    public function testExtractFail3()
+    {
+        $zipFile = new ZipFile();
+        $zipFile['file'] = 'content';
+        $zipFile->saveAsFile($this->outputFilename);
+        $zipFile->close();
+
+        $extractPath = sys_get_temp_dir() . '/zipExtractTest';
+        if (is_dir($extractPath)) {
+            FilesUtil::removeDir($extractPath);
+        }
+        self::assertTrue(mkdir($extractPath, 0444, true));
+        self::assertTrue(chmod($extractPath, 0444));
+
+        $zipFile->openFile($this->outputFilename);
+        $zipFile->extractTo($extractPath);
+    }
+
+    /**
+     * Test archive password.
+     */
+    public function testSetPassword()
+    {
+        $password = base64_encode(CryptoUtil::randomBytes(100));
+        $badPassword = "sdgt43r23wefe";
+
+        // create encryption password with ZipCrypto
+        $zipFile = new ZipFile();
+        $zipFile->addDir(__DIR__);
+        $zipFile->withNewPassword($password, ZipFile::ENCRYPTION_METHOD_TRADITIONAL);
+        $zipFile->saveAsFile($this->outputFilename);
+        $zipFile->close();
+
+        self::assertCorrectZipArchive($this->outputFilename, $password);
+
+        // check bad password for ZipCrypto
+        $zipFile->openFile($this->outputFilename);
+        $zipFile->withReadPassword($badPassword);
+        foreach ($zipFile->getListFiles() as $entryName) {
+            try {
+                $zipFile[$entryName];
+                self::fail("Expected Exception has not been raised.");
+            } catch (ZipAuthenticationException $ae) {
+                self::assertNotNull($ae);
+            }
+        }
+
+        // check correct password for ZipCrypto
+        $zipFile->withReadPassword($password);
+        foreach ($zipFile->getAllInfo() as $info) {
+            self::assertTrue($info->isEncrypted());
+            self::assertContains('ZipCrypto', $info->getMethod());
+            $decryptContent = $zipFile[$info->getPath()];
+            self::assertNotEmpty($decryptContent);
+            self::assertContains('<?php', $decryptContent);
+        }
+
+        // change encryption method to WinZip Aes and update file
+        $zipFile->withNewPassword($password, ZipFile::ENCRYPTION_METHOD_WINZIP_AES);
+        $zipFile->saveAsFile($this->outputFilename);
+        $zipFile->close();
+
+        self::assertCorrectZipArchive($this->outputFilename, $password);
+
+        // check from WinZip AES encryption
+        $zipFile->openFile($this->outputFilename);
+        // set bad password WinZip AES
+        $zipFile->withReadPassword($badPassword);
+        foreach ($zipFile->getListFiles() as $entryName) {
+            try {
+                $zipFile[$entryName];
+                self::fail("Expected Exception has not been raised.");
+            } catch (ZipAuthenticationException $ae) {
+                self::assertNotNull($ae);
+            }
+        }
+
+        // set correct password WinZip AES
+        $zipFile->withReadPassword($password);
+        foreach ($zipFile->getAllInfo() as $info) {
+            self::assertTrue($info->isEncrypted());
+            self::assertContains('WinZip', $info->getMethod());
+            $decryptContent = $zipFile[$info->getPath()];
+            self::assertNotEmpty($decryptContent);
+            self::assertContains('<?php', $decryptContent);
+        }
+
+        // clear password
+        $zipFile->addFromString('file1', '');
+        $zipFile->withoutPassword();
+        $zipFile->addFromString('file2', '');
+        $zipFile->saveAsFile($this->outputFilename);
+        $zipFile->close();
+
+        self::assertCorrectZipArchive($this->outputFilename);
+
+        // check remove password
+        $zipFile->openFile($this->outputFilename);
+        foreach ($zipFile->getAllInfo() as $info) {
+            self::assertFalse($info->isEncrypted());
+        }
+        $zipFile->close();
+    }
+
+    /**
+     * @expectedException \PhpZip\Exception\InvalidArgumentException
+     * @expectedExceptionMessage Contents is null
+     */
+    public function testAddFromStringNullContents()
+    {
+        $zipFile = new ZipFile();
+        $zipFile->addFromString('file', null);
+    }
+
+    /**
+     * @expectedException \PhpZip\Exception\InvalidArgumentException
+     * @expectedExceptionMessage Incorrect entry name
+     */
+    public function testAddFromStringNullEntryName()
+    {
+        $zipFile = new ZipFile();
+        $zipFile->addFromString(null, 'contents');
+    }
+
+    /**
+     * @expectedException \PhpZip\Exception\ZipUnsupportMethod
+     * @expectedExceptionMessage Unsupported method
+     */
+    public function testAddFromStringUnsupportedMethod()
+    {
+        $zipFile = new ZipFile();
+        $zipFile->addFromString('file', 'contents', ZipEntry::METHOD_WINZIP_AES);
+    }
+
+    /**
+     * @expectedException \PhpZip\Exception\InvalidArgumentException
+     * @expectedExceptionMessage Incorrect entry name
+     */
+    public function testAddFromStringEmptyEntryName()
+    {
+        $zipFile = new ZipFile();
+        $zipFile->addFromString('', 'contents');
+    }
+
+    /**
+     * Test compression method from add string.
+     */
+    public function testAddFromStringCompressionMethod()
+    {
+        $fileStored = sys_get_temp_dir() . '/zip-stored.txt';
+        $fileDeflated = sys_get_temp_dir() . '/zip-deflated.txt';
+
+        self::assertNotFalse(file_put_contents($fileStored, 'content'));
+        self::assertNotFalse(file_put_contents($fileDeflated, str_repeat('content', 200)));
+
+        $zipFile = new ZipFile();
+        $zipFile->addFromString(basename($fileStored), file_get_contents($fileStored));
+        $zipFile->addFromString(basename($fileDeflated), file_get_contents($fileDeflated));
+        $zipFile->saveAsFile($this->outputFilename);
+        $zipFile->close();
+
+        unlink($fileStored);
+        unlink($fileDeflated);
+
+        $zipFile->openFile($this->outputFilename);
+        $infoStored = $zipFile->getEntryInfo(basename($fileStored));
+        $infoDeflated = $zipFile->getEntryInfo(basename($fileDeflated));
+        self::assertEquals($infoStored->getMethod(), 'No compression');
+        self::assertEquals($infoDeflated->getMethod(), 'Deflate');
+        $zipFile->close();
+    }
+
+    /**
+     * @expectedException \PhpZip\Exception\InvalidArgumentException
+     * @expectedExceptionMessage stream is not resource
+     */
+    public function testAddFromStreamInvalidResource()
+    {
+        $zipFile = new ZipFile();
+        $zipFile->addFromStream("invalid resource", "name");
+    }
+
+    /**
+     * @expectedException \PhpZip\Exception\InvalidArgumentException
+     * @expectedExceptionMessage Incorrect entry name
+     */
+    public function testAddFromStreamEmptyEntryName()
+    {
+        $handle = fopen(__FILE__, 'rb');
+
+        $zipFile = new ZipFile();
+        $zipFile->addFromStream($handle, "");
+    }
+
+    /**
+     * @expectedException \PhpZip\Exception\ZipUnsupportMethod
+     * @expectedExceptionMessage Unsupported method
+     */
+    public function testAddFromStreamUnsupportedMethod()
+    {
+        $handle = fopen(__FILE__, 'rb');
+
+        $zipFile = new ZipFile();
+        $zipFile->addFromStream($handle, basename(__FILE__), ZipEntry::METHOD_WINZIP_AES);
+    }
+
+    /**
+     * Test compression method from add stream.
+     */
+    public function testAddFromStreamCompressionMethod()
+    {
+        $fileStored = sys_get_temp_dir() . '/zip-stored.txt';
+        $fileDeflated = sys_get_temp_dir() . '/zip-deflated.txt';
+
+        self::assertNotFalse(file_put_contents($fileStored, 'content'));
+        self::assertNotFalse(file_put_contents($fileDeflated, str_repeat('content', 200)));
+
+        $fpStored = fopen($fileStored, 'rb');
+        $fpDeflated = fopen($fileDeflated, 'rb');
+
+        $zipFile = new ZipFile();
+        $zipFile->addFromStream($fpStored, basename($fileStored));
+        $zipFile->addFromStream($fpDeflated, basename($fileDeflated));
+        $zipFile->saveAsFile($this->outputFilename);
+        $zipFile->close();
+
+        unlink($fileStored);
+        unlink($fileDeflated);
+
+        $zipFile->openFile($this->outputFilename);
+        $infoStored = $zipFile->getEntryInfo(basename($fileStored));
+        $infoDeflated = $zipFile->getEntryInfo(basename($fileDeflated));
+        self::assertEquals($infoStored->getMethod(), 'No compression');
+        self::assertEquals($infoDeflated->getMethod(), 'Deflate');
+        $zipFile->close();
+    }
+
+    /**
+     * @expectedException \PhpZip\Exception\InvalidArgumentException
+     * @expectedExceptionMessage Filename is null
+     */
+    public function testAddFileNullFileName()
+    {
+        $zipFile = new ZipFile();
+        $zipFile->addFile(null);
+    }
+
+    /**
+     * @expectedException \PhpZip\Exception\InvalidArgumentException
+     * @expectedExceptionMessage File is not exists
+     */
+    public function testAddFileCantExists()
+    {
+        $zipFile = new ZipFile();
+        $zipFile->addFile('path/to/file');
+    }
+
+    /**
+     * @expectedException \PhpZip\Exception\ZipUnsupportMethod
+     * @expectedExceptionMessage Unsupported method
+     */
+    public function testAddFileUnsupportedMethod()
+    {
+        $zipFile = new ZipFile();
+        $zipFile->addFile(__FILE__, null, ZipEntry::METHOD_WINZIP_AES);
+    }
+
+    /**
+     * @expectedException \PhpZip\Exception\InvalidArgumentException
+     * @expectedExceptionMessage can not open
+     */
+    public function testAddFileCantOpen()
+    {
+        self::assertNotFalse(file_put_contents($this->outputFilename, ''));
+        self::assertTrue(chmod($this->outputFilename, 0244));
+
+        $zipFile = new ZipFile();
+        $zipFile->addFile($this->outputFilename);
+    }
+
+    /**
+     * 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 = [ZipFile::METHOD_STORED, ZipFile::METHOD_DEFLATED];
+        if (extension_loaded("bz2")) {
+            $methods[] = ZipFile::METHOD_BZIP2;
+        }
+
+        $zipFile = new ZipFile();
+        $zipFile->setCompressionLevel(ZipFile::LEVEL_BEST_SPEED);
+        foreach ($files as $entryName => $content) {
+            $zipFile->addFromString($entryName, $content, $methods[array_rand($methods)]);
+        }
+        $zipFile->saveAsFile($this->outputFilename);
+        $zipFile->close();
+
+        self::assertCorrectZipArchive($this->outputFilename);
+
+        $zipFile->openFile($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();
+
+        $zipFile = new ZipFile();
+        $zipFile['file1.txt'] = 'content 1';
+        $zipFile['dir/file2.txt'] = 'content 1';
+        $zipFile['dir/empty dir/'] = null;
+        $zipFile->saveAsFile($this->outputFilename);
+        $zipFile->close();
+
+        self::assertCorrectZipArchive($this->outputFilename);
+
+        $zipFile->openFile($this->outputFilename);
+        self::assertTrue(isset($zipFile['file1.txt']));
+        self::assertTrue(isset($zipFile['dir/file2.txt']));
+        self::assertTrue(isset($zipFile['dir/empty dir/']));
+        self::assertFalse(isset($zipFile['dir/empty dir/2/']));
+        $zipFile['dir/empty dir/2/'] = null;
+        unset($zipFile['dir/file2.txt'], $zipFile['dir/empty dir/']);
+        $zipFile->saveAsFile($this->outputFilename);
+        $zipFile->close();
+
+        self::assertCorrectZipArchive($this->outputFilename);
+
+        $zipFile->openFile($this->outputFilename);
+        self::assertTrue(isset($zipFile['file1.txt']));
+        self::assertFalse(isset($zipFile['dir/file2.txt']));
+        self::assertFalse(isset($zipFile['dir/empty dir/']));
+        self::assertTrue(isset($zipFile['dir/empty dir/2/']));
+        $zipFile->close();
+    }
+
+    /**
+     * @expectedException \PhpZip\Exception\InvalidArgumentException
+     * @expectedExceptionMessage DirName empty
+     */
+    public function testAddEmptyDirNullName(){
+        $zipFile = new ZipFile();
+        $zipFile->addEmptyDir(null);
+    }
+
+    /**
+     * @expectedException \PhpZip\Exception\InvalidArgumentException
+     * @expectedExceptionMessage DirName empty
+     */
+    public function testAddEmptyDirEmptyName(){
+        $zipFile = new ZipFile();
+        $zipFile->addEmptyDir("");
+    }
+
+    /**
+     * Test zip alignment.
+     */
+    public function testZipAlign()
+    {
+        $zipFile = new ZipFile();
+        for ($i = 0; $i < 100; $i++) {
+            $zipFile->addFromString(
+                'entry' . $i . '.txt',
+                CryptoUtil::randomBytes(mt_rand(100, 4096)),
+                ZipFile::METHOD_STORED
+            );
+        }
+        $zipFile->saveAsFile($this->outputFilename);
+        $zipFile->close();
+
+        self::assertCorrectZipArchive($this->outputFilename);
+
+        $result = self::doZipAlignVerify($this->outputFilename);
+        if ($result === null) return; // zip align not installed
+
+        // check not zip align
+        self::assertFalse($result);
+
+        $zipFile->openFile($this->outputFilename);
+        $zipFile->setZipAlign(4);
+        $zipFile->saveAsFile($this->outputFilename);
+        $zipFile->close();
+
+        self::assertCorrectZipArchive($this->outputFilename);
+
+        $result = self::doZipAlignVerify($this->outputFilename, true);
+        self::assertNotNull($result);
+
+        // check zip align
+        self::assertTrue($result);
+
+        $zipFile = new ZipFile();
+        for ($i = 0; $i < 100; $i++) {
+            $zipFile->addFromString(
+                'entry' . $i . '.txt',
+                CryptoUtil::randomBytes(mt_rand(100, 4096)),
+                ZipFile::METHOD_STORED
+            );
+        }
+        $zipFile->setZipAlign(4);
+        $zipFile->saveAsFile($this->outputFilename);
+        $zipFile->close();
+
+        self::assertCorrectZipArchive($this->outputFilename);
+
+        $result = self::doZipAlignVerify($this->outputFilename);
+        // check not zip align
+        self::assertTrue($result);
+    }
+
+    /**
+     * Test support ZIP64 ext (slow test - normal).
+     * Create > 65535 files in archive and open and extract to /dev/null.
+     */
+    public function testCreateAndOpenZip64Ext()
+    {
+        $countFiles = 0xffff + 1;
+
+        $zipFile = new ZipFile();
+        for ($i = 0; $i < $countFiles; $i++) {
+            $zipFile[$i . '.txt'] = $i;
+        }
+        $zipFile->saveAsFile($this->outputFilename);
+        $zipFile->close();
+
+        self::assertCorrectZipArchive($this->outputFilename);
+
+        $zipFile->openFile($this->outputFilename);
+        self::assertEquals($zipFile->count(), $countFiles);
+        foreach ($zipFile as $entry => $content) {
+
+        }
+        $zipFile->close();
+    }
+
+}

+ 0 - 1093
tests/PhpZip/ZipTest.php

@@ -1,1093 +0,0 @@
-<?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);
-
-        $outputZipFile = ZipOutputFile::openFromFile($this->outputFilename);
-        $outputZipFile->rename($oldName, $newName);
-        $outputZipFile->saveAsFile($this->outputFilename);
-
-        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 = $zipFile->edit();
-        $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 = base64_encode(CryptoUtil::randomBytes(100));
-        $badPassword = "sdgt43r23wefe";
-
-        $outputZip = ZipOutputFile::create();
-        $outputZip->addDir(__DIR__);
-        $outputZip->setPassword($password, ZipEntry::ENCRYPTION_METHOD_TRADITIONAL);
-        $outputZip->saveAsFile($this->outputFilename);
-        $outputZip->close();
-
-        self::assertCorrectZipArchive($this->outputFilename, $password);
-
-        $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();
-
-        self::assertCorrectZipArchive($this->outputFilename, $password);
-
-        // 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' => extension_loaded("bz2") ? ZipEntry::METHOD_BZIP2 : ZipEntry::METHOD_STORED,
-            ],
-            '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 zip alignment.
-     */
-    public function testZipAlign()
-    {
-        $zipOutputFile = ZipOutputFile::create();
-
-        for ($i = 0; $i < 100; $i++) {
-            $zipOutputFile->addFromString(
-                'entry' . $i . '.txt',
-                CryptoUtil::randomBytes(mt_rand(100, 4096)),
-                ZipEntry::METHOD_STORED
-            );
-        }
-        $zipOutputFile->saveAsFile($this->outputFilename);
-        $zipOutputFile->close();
-
-        self::assertCorrectZipArchive($this->outputFilename);
-
-        $result = self::doZipAlignVerify($this->outputFilename);
-        if($result === null) return; // zip align not installed
-
-        // check not zip align
-        self::assertFalse($result);
-
-        $zipFile = ZipFile::openFromFile($this->outputFilename);
-        $zipOutputFile = ZipOutputFile::openFromZipFile($zipFile);
-        $zipOutputFile->setZipAlign(4);
-        $zipOutputFile->saveAsFile($this->outputFilename);
-        $zipOutputFile->close();
-        $zipFile->close();
-
-        self::assertCorrectZipArchive($this->outputFilename);
-
-        $result = self::doZipAlignVerify($this->outputFilename);
-        self::assertNotNull($result);
-
-        // check zip align
-        self::assertTrue($result);
-    }
-
-    /**
-     * Test support ZIP64 ext (slow test - normal).
-     * Create > 65535 files in archive and open and extract to /dev/null.
-     */
-    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 as $entry => $content) {
-            strlen($content);
-        }
-        $zipFile->close();
-    }
-
-}

+ 12 - 11
tests/PhpZip/ZipTestCase.php

@@ -1,5 +1,6 @@
 <?php
 namespace PhpZip;
+use PhpZip\Model\EndOfCentralDirectory;
 
 /**
  * PHPUnit test case and helper methods.
@@ -25,12 +26,12 @@ class ZipTestCase extends \PHPUnit_Framework_TestCase
             $output = implode(PHP_EOL, $output);
 
             if ($password !== null && $returnCode === 81) {
-                if(`which 7z`){
+                if (`which 7z`) {
                     // WinZip 99-character limit
                     // @see https://sourceforge.net/p/p7zip/discussion/383044/thread/c859a2f0/
                     $password = substr($password, 0, 99);
 
-                    $command = "7z t -p" . escapeshellarg($password). " " . escapeshellarg($filename);
+                    $command = "7z t -p" . escapeshellarg($password) . " " . escapeshellarg($filename);
                     exec($command, $output, $returnCode);
 
                     $output = implode(PHP_EOL, $output);
@@ -38,14 +39,12 @@ class ZipTestCase extends \PHPUnit_Framework_TestCase
                     self::assertEquals($returnCode, 0);
                     self::assertNotContains(' Errors', $output);
                     self::assertContains(' Ok', $output);
+                } else {
+                    fwrite(STDERR, 'Program unzip cannot support this function.' . PHP_EOL);
+                    fwrite(STDERR, 'Please install 7z. For Ubuntu-like: sudo apt-get install p7zip-full' . PHP_EOL);
                 }
-                else{
-                    fwrite(STDERR, 'Program unzip cannot support this function.'.PHP_EOL);
-                    fwrite(STDERR, 'Please install 7z. For Ubuntu-like: sudo apt-get install p7zip-full'.PHP_EOL);
-                }
-            }
-            else {
-                self::assertEquals($returnCode, 0);
+            } else {
+                self::assertEquals($returnCode, 0, $output);
                 self::assertNotContains('incorrect password', $output);
                 self::assertContains(' OK', $output);
                 self::assertContains('No errors', $output);
@@ -67,18 +66,20 @@ class ZipTestCase extends \PHPUnit_Framework_TestCase
 
             self::assertContains('Empty zipfile', $output);
         }
-        $actualEmptyZipData = pack('VVVVVv', ZipConstants::END_OF_CENTRAL_DIRECTORY_RECORD_SIG, 0, 0, 0, 0, 0);
+        $actualEmptyZipData = pack('VVVVVv', EndOfCentralDirectory::END_OF_CENTRAL_DIRECTORY_RECORD_SIG, 0, 0, 0, 0, 0);
         self::assertEquals(file_get_contents($filename), $actualEmptyZipData);
     }
 
     /**
      * @param string $filename
+     * @param bool $showErrors
      * @return bool|null If null - can not install zipalign
      */
-    public static function doZipAlignVerify($filename)
+    public static function doZipAlignVerify($filename, $showErrors = false)
     {
         if (DIRECTORY_SEPARATOR !== '\\' && `which zipalign`) {
             exec("zipalign -c -v 4 " . escapeshellarg($filename), $output, $returnCode);
+            if ($showErrors && $returnCode !== 0) fwrite(STDERR, implode(PHP_EOL, $output));
             return $returnCode === 0;
         } else {
             fwrite(STDERR, 'Can not find program "zipalign" for test');

Certains fichiers n'ont pas été affichés car il y a eu trop de fichiers modifiés dans ce diff