ソースを参照

minor fixes, ZipEntry tests

wapplay 6 年 前
コミット
a16b0e7c15

+ 2 - 1
.gitattributes

@@ -1,7 +1,8 @@
 .gitattributes export-ignore
 .github export-ignore
 .gitignore export-ignore
-.travis.yml export-ignore
 .php_cs export-ignore
+.travis.yml export-ignore
+bootstrap.php export-ignore
 phpunit.xml export-ignore
 tests export-ignore

+ 50 - 2
.phpstorm.meta.php

@@ -26,6 +26,8 @@ namespace PHPSTORM_META {
     expectedArguments(\PhpZip\ZipFile::addFilesFromRegex(), 3, argumentsSet("compression_methods"));
     expectedArguments(\PhpZip\ZipFile::addFilesFromRegexRecursive(), 3, argumentsSet("compression_methods"));
     expectedArguments(\PhpZip\ZipFile::setCompressionMethodEntry(), 1, argumentsSet("compression_methods"));
+    expectedArguments(\PhpZip\Model\ZipEntry::setCompressionMethod(), 0, argumentsSet("compression_methods"));
+    expectedArguments(\PhpZip\Model\ZipEntry::setMethod(), 0, argumentsSet("compression_methods"));
 
     registerArgumentsSet(
         'compression_levels',
@@ -36,6 +38,7 @@ namespace PHPSTORM_META {
     );
     expectedArguments(\PhpZip\ZipFile::setCompressionLevel(), 0, argumentsSet("compression_levels"));
     expectedArguments(\PhpZip\ZipFile::setCompressionLevelEntry(), 1, argumentsSet("compression_levels"));
+    expectedArguments(\PhpZip\Model\ZipEntry::setCompressionLevel(), 0, argumentsSet("compression_levels"));
 
     registerArgumentsSet(
         'encryption_methods',
@@ -46,6 +49,8 @@ namespace PHPSTORM_META {
     );
     expectedArguments(\PhpZip\ZipFile::setPassword(), 1, argumentsSet("encryption_methods"));
     expectedArguments(\PhpZip\ZipFile::setPasswordEntry(), 2, argumentsSet("encryption_methods"));
+    expectedArguments(\PhpZip\Model\ZipEntry::setEncryptionMethod(), 0, argumentsSet("encryption_methods"));
+    expectedArguments(\PhpZip\Model\ZipEntry::setPassword(), 1, argumentsSet("encryption_methods"));
 
     registerArgumentsSet(
         'zip_mime_types',
@@ -57,6 +62,49 @@ namespace PHPSTORM_META {
     expectedArguments(\PhpZip\ZipFile::outputAsAttachment(), 1, argumentsSet("zip_mime_types"));
     expectedArguments(\PhpZip\ZipFile::outputAsAttachment(), 2, argumentsSet("bool"));
 
-    expectedArguments(\PhpZip\ZipFileI::outputAsResponse(), 2, argumentsSet("zip_mime_types"));
-    expectedArguments(\PhpZip\ZipFileI::outputAsResponse(), 3, argumentsSet("bool"));
+    expectedArguments(\PhpZip\ZipFile::outputAsResponse(), 2, argumentsSet("zip_mime_types"));
+    expectedArguments(\PhpZip\ZipFile::outputAsResponse(), 3, argumentsSet("bool"));
+
+    registerArgumentsSet(
+        'dos_charset',
+        \PhpZip\Constants\DosCodePage::CP_LATIN_US,
+        \PhpZip\Constants\DosCodePage::CP_GREEK,
+        \PhpZip\Constants\DosCodePage::CP_BALT_RIM,
+        \PhpZip\Constants\DosCodePage::CP_LATIN1,
+        \PhpZip\Constants\DosCodePage::CP_LATIN2,
+        \PhpZip\Constants\DosCodePage::CP_CYRILLIC,
+        \PhpZip\Constants\DosCodePage::CP_TURKISH,
+        \PhpZip\Constants\DosCodePage::CP_PORTUGUESE,
+        \PhpZip\Constants\DosCodePage::CP_ICELANDIC,
+        \PhpZip\Constants\DosCodePage::CP_HEBREW,
+        \PhpZip\Constants\DosCodePage::CP_CANADA,
+        \PhpZip\Constants\DosCodePage::CP_ARABIC,
+        \PhpZip\Constants\DosCodePage::CP_NORDIC,
+        \PhpZip\Constants\DosCodePage::CP_CYRILLIC_RUSSIAN,
+        \PhpZip\Constants\DosCodePage::CP_GREEK2,
+        \PhpZip\Constants\DosCodePage::CP_THAI,
+    );
+    expectedArguments(\PhpZip\Model\ZipEntry::setCharset(), 0, argumentsSet('dos_charset'));
+    expectedArguments(\PhpZip\Constants\DosCodePage::toUTF8(), 1, argumentsSet('dos_charset'));
+    expectedArguments(\PhpZip\Constants\DosCodePage::fromUTF8(), 1, argumentsSet('dos_charset'));
+
+    registerArgumentsSet(
+        "zip_os",
+        \PhpZip\Constants\ZipPlatform::OS_UNIX,
+        \PhpZip\Constants\ZipPlatform::OS_DOS,
+        \PhpZip\Constants\ZipPlatform::OS_MAC_OSX,
+    );
+    expectedArguments(\PhpZip\Model\ZipEntry::setCreatedOS(), 0, argumentsSet('zip_os'));
+    expectedArguments(\PhpZip\Model\ZipEntry::setExtractedOS(), 0, argumentsSet('zip_os'));
+    expectedArguments(\PhpZip\Model\ZipEntry::setPlatform(), 0, argumentsSet('zip_os'));
+
+    registerArgumentsSet(
+        "zip_gpbf",
+        \PhpZip\Constants\GeneralPurposeBitFlag::ENCRYPTION |
+        \PhpZip\Constants\GeneralPurposeBitFlag::DATA_DESCRIPTOR |
+        \PhpZip\Constants\GeneralPurposeBitFlag::COMPRESSION_FLAG1 |
+        \PhpZip\Constants\GeneralPurposeBitFlag::COMPRESSION_FLAG2 |
+        \PhpZip\Constants\GeneralPurposeBitFlag::UTF8
+    );
+    expectedArguments(\PhpZip\Model\ZipEntry::setGeneralPurposeBitFlags(), 0, argumentsSet('zip_gpbf'));
 }

+ 6 - 0
bootstrap.php

@@ -0,0 +1,6 @@
+<?php
+
+// see https://stackoverflow.com/questions/33299149/phpstorm-8-and-phpunit-problems-with-runinseparateprocess/37174348#37174348
+if (!defined('PHPUNIT_COMPOSER_INSTALL')) {
+    define('PHPUNIT_COMPOSER_INSTALL', __DIR__ . '/vendor/autoload.php');
+}

+ 1 - 1
phpunit.xml

@@ -4,7 +4,7 @@
          xsi:noNamespaceSchemaLocation="http://schema.phpunit.de/4.8/phpunit.xsd"
          backupGlobals="false"
          colors="true"
-         bootstrap="vendor/autoload.php">
+         bootstrap="bootstrap.php">
     <php>
         <ini name="error_reporting" value="-1"/>
     </php>

+ 1 - 1
src/IO/Filter/Cipher/Traditional/PKCryptContext.php → src/IO/Filter/Cipher/Pkware/PKCryptContext.php

@@ -1,6 +1,6 @@
 <?php
 
-namespace PhpZip\IO\Filter\Cipher\Traditional;
+namespace PhpZip\IO\Filter\Cipher\Pkware;
 
 use PhpZip\Exception\RuntimeException;
 use PhpZip\Exception\ZipAuthenticationException;

+ 1 - 1
src/IO/Filter/Cipher/Traditional/PKDecryptionStreamFilter.php → src/IO/Filter/Cipher/Pkware/PKDecryptionStreamFilter.php

@@ -1,6 +1,6 @@
 <?php
 
-namespace PhpZip\IO\Filter\Cipher\Traditional;
+namespace PhpZip\IO\Filter\Cipher\Pkware;
 
 use PhpZip\Exception\ZipException;
 use PhpZip\Model\ZipEntry;

+ 2 - 2
src/IO/Filter/Cipher/Traditional/PKEncryptionStreamFilter.php → src/IO/Filter/Cipher/Pkware/PKEncryptionStreamFilter.php

@@ -1,6 +1,6 @@
 <?php
 
-namespace PhpZip\IO\Filter\Cipher\Traditional;
+namespace PhpZip\IO\Filter\Cipher\Pkware;
 
 use PhpZip\Exception\RuntimeException;
 use PhpZip\Model\ZipEntry;
@@ -66,7 +66,7 @@ class PKEncryptionStreamFilter extends \php_user_filter
         // init keys
         $this->context = new PKCryptContext($password);
 
-        $crc = $entry->isDataDescriptorRequired() ?
+        $crc = $entry->isDataDescriptorRequired() || $entry->getCrc() === ZipEntry::UNKNOWN ?
             ($entry->getDosTime() & 0x0000ffff) << 16 :
             $entry->getCrc();
 

+ 2 - 2
src/IO/ZipReader.php

@@ -11,7 +11,7 @@ use PhpZip\Constants\ZipOptions;
 use PhpZip\Exception\Crc32Exception;
 use PhpZip\Exception\InvalidArgumentException;
 use PhpZip\Exception\ZipException;
-use PhpZip\IO\Filter\Cipher\Traditional\PKDecryptionStreamFilter;
+use PhpZip\IO\Filter\Cipher\Pkware\PKDecryptionStreamFilter;
 use PhpZip\IO\Filter\Cipher\WinZipAes\WinZipAesDecryptionStreamFilter;
 use PhpZip\Model\Data\ZipSourceFileData;
 use PhpZip\Model\EndOfCentralDirectory;
@@ -713,7 +713,7 @@ class ZipReader
             throw new InvalidArgumentException('outStream is not resource');
         }
 
-        $entry = $zipFileData->getZipEntry();
+        $entry = $zipFileData->getSourceEntry();
 
 //        if ($entry->isDirectory()) {
 //            throw new InvalidArgumentException('Streams not supported for directories');

+ 2 - 4
src/IO/ZipWriter.php

@@ -10,7 +10,7 @@ use PhpZip\Constants\ZipPlatform;
 use PhpZip\Constants\ZipVersion;
 use PhpZip\Exception\ZipException;
 use PhpZip\Exception\ZipUnsupportMethodException;
-use PhpZip\IO\Filter\Cipher\Traditional\PKEncryptionStreamFilter;
+use PhpZip\IO\Filter\Cipher\Pkware\PKEncryptionStreamFilter;
 use PhpZip\IO\Filter\Cipher\WinZipAes\WinZipAesEncryptionStreamFilter;
 use PhpZip\Model\Data\ZipSourceFileData;
 use PhpZip\Model\Extra\Fields\ApkAlignmentExtraField;
@@ -109,7 +109,6 @@ class ZipWriter
         $compressedSize = $entry->getCompressedSize();
         $uncompressedSize = $entry->getUncompressedSize();
 
-        // todo check on 32bit system
         $entry->getLocalExtraFields()->remove(Zip64ExtraField::HEADER_ID);
 
         if ($compressedSize > ZipConstants::ZIP64_MAGIC || $uncompressedSize > ZipConstants::ZIP64_MAGIC) {
@@ -331,7 +330,7 @@ class ZipWriter
         //     (PHP cannot apply the filter for encryption after the compression
         //     filter, so a temporary stream is created for the compressed data)
 
-        if ($zipData instanceof ZipSourceFileData && !$this->zipContainer->hasRecompressData($entry)) {
+        if ($zipData instanceof ZipSourceFileData && !$zipData->hasRecompressData($entry)) {
             // data of source zip file -> copy compressed data
             $zipData->copyCompressedDataToStream($outStream);
 
@@ -631,7 +630,6 @@ class ZipWriter
         $uncompressedSize = $entry->getUncompressedSize();
         $localHeaderOffset = $entry->getLocalHeaderOffset();
 
-        // todo check on 32bit system
         $entry->getCdExtraFields()->remove(Zip64ExtraField::HEADER_ID);
 
         if (

+ 21 - 4
src/Model/Data/ZipSourceFileData.php

@@ -20,7 +20,7 @@ class ZipSourceFileData implements ZipData
     private $stream;
 
     /** @var ZipEntry */
-    private $zipEntry;
+    private $sourceEntry;
 
     /** @var int */
     private $offset;
@@ -42,11 +42,28 @@ class ZipSourceFileData implements ZipData
     {
         $this->zipReader = $zipReader;
         $this->offset = $offsetData;
-        $this->zipEntry = $zipEntry;
+        $this->sourceEntry = $zipEntry;
         $this->compressedSize = $zipEntry->getCompressedSize();
         $this->uncompressedSize = $zipEntry->getUncompressedSize();
     }
 
+    /**
+     * @param ZipEntry $entry
+     *
+     * @return bool
+     */
+    public function hasRecompressData(ZipEntry $entry)
+    {
+        return $this->sourceEntry->getCompressionLevel() !== $entry->getCompressionLevel() ||
+            $this->sourceEntry->getCompressionMethod() !== $entry->getCompressionMethod() ||
+            $this->sourceEntry->isEncrypted() !== $entry->isEncrypted() ||
+            $this->sourceEntry->getEncryptionMethod() !== $entry->getEncryptionMethod() ||
+            $this->sourceEntry->getPassword() !== $entry->getPassword() ||
+            $this->sourceEntry->getCompressedSize() !== $entry->getCompressedSize() ||
+            $this->sourceEntry->getUncompressedSize() !== $entry->getUncompressedSize() ||
+            $this->sourceEntry->getCrc() !== $entry->getCrc();
+    }
+
     /**
      * @throws ZipException
      *
@@ -114,9 +131,9 @@ class ZipSourceFileData implements ZipData
     /**
      * @return ZipEntry
      */
-    public function getZipEntry()
+    public function getSourceEntry()
     {
-        return $this->zipEntry;
+        return $this->sourceEntry;
     }
 
     /**

+ 27 - 11
src/Model/Extra/Fields/NtfsExtraField.php

@@ -33,13 +33,13 @@ class NtfsExtraField implements ZipExtraField
      */
     const EPOCH_OFFSET = -11644473600;
 
-    /** @var int Modify time timestamp */
+    /** @var int Modify ntfs time */
     private $modifyTime;
 
-    /** @var int Access time timestamp */
+    /** @var int Access ntfs time */
     private $accessTime;
 
-    /** @var int Create time timestamp */
+    /** @var int Create ntfs time */
     private $createTime;
 
     /**
@@ -54,6 +54,22 @@ class NtfsExtraField implements ZipExtraField
         $this->createTime = (int) $createTime;
     }
 
+    /**
+     * @param \DateTimeInterface $mtime
+     * @param \DateTimeInterface $atime
+     * @param \DateTimeInterface $ctime
+     *
+     * @return NtfsExtraField
+     */
+    public static function create(\DateTimeInterface $mtime, \DateTimeInterface $atime, \DateTimeInterface $ctime)
+    {
+        return new self(
+            self::dateTimeToNtfsTime($mtime),
+            self::dateTimeToNtfsTime($atime),
+            self::dateTimeToNtfsTime($ctime)
+        );
+    }
+
     /**
      * Returns the Header ID (type) of this Extra Field.
      * The Header ID is an unsigned short integer (two bytes)
@@ -145,7 +161,7 @@ class NtfsExtraField implements ZipExtraField
      */
     public function getModifyDateTime()
     {
-        return $this->ntfsTimeToDateTime($this->modifyTime);
+        return self::ntfsTimeToDateTime($this->modifyTime);
     }
 
     /**
@@ -153,7 +169,7 @@ class NtfsExtraField implements ZipExtraField
      */
     public function setModifyDateTime(\DateTimeInterface $modifyTime)
     {
-        $this->modifyTime = $this->dateTimeToNtfsTime($modifyTime);
+        $this->modifyTime = self::dateTimeToNtfsTime($modifyTime);
     }
 
     /**
@@ -161,7 +177,7 @@ class NtfsExtraField implements ZipExtraField
      */
     public function getAccessDateTime()
     {
-        return $this->ntfsTimeToDateTime($this->accessTime);
+        return self::ntfsTimeToDateTime($this->accessTime);
     }
 
     /**
@@ -169,7 +185,7 @@ class NtfsExtraField implements ZipExtraField
      */
     public function setAccessDateTime(\DateTimeInterface $accessTime)
     {
-        $this->accessTime = $this->dateTimeToNtfsTime($accessTime);
+        $this->accessTime = self::dateTimeToNtfsTime($accessTime);
     }
 
     /**
@@ -177,7 +193,7 @@ class NtfsExtraField implements ZipExtraField
      */
     public function getCreateDateTime()
     {
-        return $this->ntfsTimeToDateTime($this->createTime);
+        return self::ntfsTimeToDateTime($this->createTime);
     }
 
     /**
@@ -185,7 +201,7 @@ class NtfsExtraField implements ZipExtraField
      */
     public function setCreateDateTime(\DateTimeInterface $createTime)
     {
-        $this->createTime = $this->dateTimeToNtfsTime($createTime);
+        $this->createTime = self::dateTimeToNtfsTime($createTime);
     }
 
     /**
@@ -193,7 +209,7 @@ class NtfsExtraField implements ZipExtraField
      *
      * @return int
      */
-    protected function dateTimeToNtfsTime(\DateTimeInterface $dateTime)
+    public static function dateTimeToNtfsTime(\DateTimeInterface $dateTime)
     {
         return $dateTime->getTimestamp() * 10000000 + self::EPOCH_OFFSET;
     }
@@ -203,7 +219,7 @@ class NtfsExtraField implements ZipExtraField
      *
      * @return \DateTimeInterface
      */
-    protected function ntfsTimeToDateTime($time)
+    public static function ntfsTimeToDateTime($time)
     {
         $timestamp = (int) ($time / 10000000 + self::EPOCH_OFFSET);
 

+ 0 - 28
src/Model/ZipContainer.php

@@ -281,34 +281,6 @@ class ZipContainer extends ImmutableZipContainer
         $this->archiveComment = $archiveComment;
     }
 
-    /**
-     * @param ZipEntry $entry
-     *
-     * @return bool
-     */
-    public function hasRecompressData(ZipEntry $entry)
-    {
-        // todo test with rename, check exists ZipSourceData
-        if ($this->sourceContainer && isset($this->sourceContainer->entries[$entry->getName()])) {
-            $sourceEntry = $this->sourceContainer->entries[$entry->getName()];
-
-            if (
-                $sourceEntry->getCompressionLevel() !== $entry->getCompressionLevel() ||
-                $sourceEntry->getCompressionMethod() !== $entry->getCompressionMethod() ||
-                $sourceEntry->isEncrypted() !== $entry->isEncrypted() ||
-                $sourceEntry->getEncryptionMethod() !== $entry->getEncryptionMethod() ||
-                $sourceEntry->getPassword() !== $entry->getPassword() ||
-                $sourceEntry->getCompressedSize() !== $entry->getCompressedSize() ||
-                $sourceEntry->getUncompressedSize() !== $entry->getUncompressedSize() ||
-                $sourceEntry->getCrc() !== $entry->getCrc()
-            ) {
-                return true;
-            }
-        }
-
-        return false;
-    }
-
     /**
      * @return ZipEntryMatcher
      */

+ 146 - 45
src/Model/ZipEntry.php

@@ -31,9 +31,9 @@ use PhpZip\Util\StringUtil;
 /**
  * ZIP file entry.
  *
- * @see https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT .ZIP File Format Specification
+ * @see     https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT .ZIP File Format Specification
  *
- * @author Ne-Lexa alexey@nelexa.ru
+ * @author  Ne-Lexa alexey@nelexa.ru
  * @license MIT
  */
 class ZipEntry
@@ -152,11 +152,12 @@ class ZipEntry
     /**
      * ZipEntry constructor.
      *
-     * @param string $name Entry name
+     * @param string      $name    Entry name
+     * @param string|null $charset DOS charset
      */
-    public function __construct($name)
+    public function __construct($name, $charset = null)
     {
-        $this->setName($name);
+        $this->setName($name, $charset);
 
         $this->cdExtraFields = new ExtraFieldsCollection();
         $this->localExtraFields = new ExtraFieldsCollection();
@@ -185,6 +186,7 @@ class ZipEntry
      * @return ZipEntry
      *
      * @internal
+     *
      * @noinspection PhpTooManyParametersInspection
      */
     public static function create(
@@ -229,11 +231,12 @@ class ZipEntry
     /**
      * Set entry name.
      *
-     * @param string $name New entry name
+     * @param string      $name    New entry name
+     * @param string|null $charset
      *
      * @return ZipEntry
      */
-    private function setName($name)
+    private function setName($name, $charset = null)
     {
         if ($name === null) {
             throw new InvalidArgumentException('zip entry name is null');
@@ -252,13 +255,24 @@ class ZipEntry
             throw new InvalidArgumentException('Illegal zip entry name parameter');
         }
 
-        if (!StringUtil::isASCII($name)) {
+        $this->setCharset($charset);
+
+        if ($this->charset === null && !StringUtil::isASCII($name)) {
             $this->enableUtf8Name(true);
         }
         $this->name = $name;
         $this->isDirectory = ($length = \strlen($name)) >= 1 && $name[$length - 1] === '/';
         $this->externalAttributes = $this->isDirectory ? DosAttrs::DOS_DIRECTORY : DosAttrs::DOS_ARCHIVE;
 
+        if ($this->extractVersion !== self::UNKNOWN) {
+            $this->extractVersion = max(
+                $this->extractVersion,
+                $this->isDirectory ?
+                    ZipVersion::v20_DEFLATED_FOLDER_ZIPCRYPTO :
+                    ZipVersion::v10_DEFAULT_MIN
+            );
+        }
+
         return $this;
     }
 
@@ -291,6 +305,8 @@ class ZipEntry
      * @param string $newName New entry name
      *
      * @return ZipEntry new {@see ZipEntry} object with new name
+     *
+     * @internal
      */
     public function rename($newName)
     {
@@ -314,6 +330,8 @@ class ZipEntry
 
     /**
      * @return ZipData|null
+     *
+     * @internal
      */
     public function getData()
     {
@@ -322,6 +340,8 @@ class ZipEntry
 
     /**
      * @param ZipData|null $data
+     *
+     * @internal
      */
     public function setData($data)
     {
@@ -458,7 +478,7 @@ class ZipEntry
                 return ZipVersion::v51_ENCR_AES_RC2_CORRECT;
             }
 
-            if ($this->getCompressionMethod() === ZipCompressionMethod::BZIP2) {
+            if ($this->compressionMethod === ZipCompressionMethod::BZIP2) {
                 return ZipVersion::v46_BZIP2;
             }
 
@@ -466,9 +486,15 @@ class ZipEntry
                 return ZipVersion::v45_ZIP64_EXT;
             }
 
-            return $this->getCompressionMethod() === ZipCompressionMethod::DEFLATED || $this->isDirectory() ?
-                ZipVersion::v20_DEFLATED_FOLDER_ZIPCRYPTO :
-                ZipVersion::v10_DEFAULT_MIN;
+            if (
+                $this->compressionMethod === ZipCompressionMethod::DEFLATED ||
+                $this->isDirectory ||
+                $this->encryptionMethod === ZipEncryptionMethod::PKWARE
+            ) {
+                return ZipVersion::v20_DEFLATED_FOLDER_ZIPCRYPTO;
+            }
+
+            return ZipVersion::v10_DEFAULT_MIN;
         }
 
         return $this->extractVersion;
@@ -520,9 +546,16 @@ class ZipEntry
      * @param int $compressedSize the Compressed Size
      *
      * @return ZipEntry
+     *
+     * @internal
      */
     public function setCompressedSize($compressedSize)
     {
+        $compressedSize = (int) $compressedSize;
+
+        if ($compressedSize < self::UNKNOWN) {
+            throw new InvalidArgumentException('Compressed size < ' . self::UNKNOWN);
+        }
         $this->compressedSize = $compressedSize;
 
         return $this;
@@ -550,6 +583,8 @@ class ZipEntry
      * @return ZipEntry
      *
      * @deprecated Use {@see ZipEntry::setUncompressedSize()}
+     *
+     * @internal
      */
     public function setSize($size)
     {
@@ -574,9 +609,16 @@ class ZipEntry
      * @param int $uncompressedSize the (Uncompressed) Size
      *
      * @return ZipEntry
+     *
+     * @internal
      */
     public function setUncompressedSize($uncompressedSize)
     {
+        $uncompressedSize = (int) $uncompressedSize;
+
+        if ($uncompressedSize < self::UNKNOWN) {
+            throw new InvalidArgumentException('Uncompressed size < ' . self::UNKNOWN);
+        }
         $this->uncompressedSize = $uncompressedSize;
 
         return $this;
@@ -596,10 +638,17 @@ class ZipEntry
      * @param int $localHeaderOffset
      *
      * @return ZipEntry
+     *
+     * @internal
      */
     public function setLocalHeaderOffset($localHeaderOffset)
     {
-        $this->localHeaderOffset = (int) $localHeaderOffset;
+        $localHeaderOffset = (int) $localHeaderOffset;
+
+        if ($localHeaderOffset < 0) {
+            throw new InvalidArgumentException('Negative $localHeaderOffset');
+        }
+        $this->localHeaderOffset = $localHeaderOffset;
 
         return $this;
     }
@@ -627,6 +676,8 @@ class ZipEntry
      * @return ZipEntry
      *
      * @deprecated Use {@see ZipEntry::setLocalHeaderOffset()}
+     *
+     * @internal
      */
     public function setOffset($offset)
     {
@@ -645,24 +696,26 @@ class ZipEntry
      */
     public function getGeneralPurposeBitFlags()
     {
-        return $this->generalPurposeBitFlags & 0xffff;
+        return $this->generalPurposeBitFlags;
     }
 
     /**
      * Sets the General Purpose Bit Flags.
      *
-     * @param mixed $general
+     * @param int $gpbf general purpose bit flags
      *
      * @return ZipEntry
      *
-     * @var int general
+     * @internal
      */
-    public function setGeneralPurposeBitFlags($general)
+    public function setGeneralPurposeBitFlags($gpbf)
     {
-        if ($general < 0x0000 || $general > 0xffff) {
-            throw new InvalidArgumentException('general out of range');
+        $gpbf = (int) $gpbf;
+
+        if ($gpbf < 0x0000 || $gpbf > 0xffff) {
+            throw new InvalidArgumentException('general purpose bit flags out of range');
         }
-        $this->generalPurposeBitFlags = $general;
+        $this->generalPurposeBitFlags = $gpbf;
         $this->updateCompressionLevel();
 
         return $this;
@@ -710,7 +763,7 @@ class ZipEntry
      */
     private function isSetGeneralBitFlag($mask)
     {
-        return ($this->generalPurposeBitFlags & $mask) !== 0;
+        return ($this->generalPurposeBitFlags & $mask) === $mask;
     }
 
     /**
@@ -775,7 +828,9 @@ class ZipEntry
     {
         $this->setEncrypted(false);
         $this->removeExtraField(WinZipAesExtraField::HEADER_ID);
+        $this->encryptionMethod = ZipEncryptionMethod::NONE;
         $this->password = null;
+        $this->extractVersion = self::UNKNOWN;
 
         return $this;
     }
@@ -858,7 +913,9 @@ class ZipEntry
      */
     public function setCompressionMethod($compressionMethod)
     {
-        if (($compressionMethod < 0x0000 || $compressionMethod > 0xffff) && $compressionMethod !== self::UNKNOWN) {
+        $compressionMethod = (int) $compressionMethod;
+
+        if ($compressionMethod < 0x0000 || $compressionMethod > 0xffff) {
             throw new InvalidArgumentException('method out of range: ' . $compressionMethod);
         }
 
@@ -866,6 +923,7 @@ class ZipEntry
 
         $this->compressionMethod = $compressionMethod;
         $this->updateCompressionLevel();
+        $this->extractVersion = self::UNKNOWN;
 
         return $this;
     }
@@ -881,7 +939,7 @@ class ZipEntry
             return self::UNKNOWN;
         }
 
-        return DateTimeConverter::toUnixTimestamp($this->getDosTime());
+        return DateTimeConverter::msDosToUnix($this->getDosTime());
     }
 
     /**
@@ -922,10 +980,8 @@ class ZipEntry
      */
     public function setTime($unixTimestamp)
     {
-        $known = $unixTimestamp !== self::UNKNOWN;
-
-        if ($known) {
-            $this->dosTime = DateTimeConverter::toDosTime($unixTimestamp);
+        if ($unixTimestamp !== self::UNKNOWN) {
+            $this->setDosTime(DateTimeConverter::unixToMsDos($unixTimestamp));
         } else {
             $this->dosTime = 0;
         }
@@ -954,6 +1010,11 @@ class ZipEntry
     {
         $this->externalAttributes = (int) $externalAttributes;
 
+        if ($externalAttributes < 0x00000000 || $externalAttributes > 0xffffffff) {
+            throw new InvalidArgumentException('external attributes out of range: ' . $externalAttributes);
+        }
+        $this->externalAttributes = $externalAttributes;
+
         return $this;
     }
 
@@ -970,13 +1031,18 @@ class ZipEntry
     /**
      * Sets the internal file attributes.
      *
-     * @param int $attributes the internal file attributes
+     * @param int $internalAttributes the internal file attributes
      *
      * @return ZipEntry
      */
-    public function setInternalAttributes($attributes)
+    public function setInternalAttributes($internalAttributes)
     {
-        $this->internalAttributes = (int) $attributes;
+        $internalAttributes = (int) $internalAttributes;
+
+        if ($internalAttributes < 0x0000 || $internalAttributes > 0xffff) {
+            throw new InvalidArgumentException('internal attributes out of range');
+        }
+        $this->internalAttributes = $internalAttributes;
 
         return $this;
     }
@@ -1094,6 +1160,31 @@ class ZipEntry
         $this->localExtraFields->remove($headerId);
     }
 
+    /**
+     * @param ZipExtraField $zipExtraField
+     */
+    public function addExtraField(ZipExtraField $zipExtraField)
+    {
+        $this->addLocalExtraField($zipExtraField);
+        $this->addCdExtraField($zipExtraField);
+    }
+
+    /**
+     * @param ZipExtraField $zipExtraField
+     */
+    public function addLocalExtraField(ZipExtraField $zipExtraField)
+    {
+        $this->localExtraFields->add($zipExtraField);
+    }
+
+    /**
+     * @param ZipExtraField $zipExtraField
+     */
+    public function addCdExtraField(ZipExtraField $zipExtraField)
+    {
+        $this->cdExtraFields->add($zipExtraField);
+    }
+
     /**
      * Returns comment entry.
      *
@@ -1116,11 +1207,11 @@ class ZipEntry
         if ($comment !== null) {
             $commentLength = \strlen($comment);
 
-            if ($commentLength < 0x0000 || $commentLength > 0xffff) {
+            if ($commentLength > 0xffff) {
                 throw new InvalidArgumentException('Comment too long');
             }
 
-            if (!StringUtil::isASCII($comment)) {
+            if ($this->charset === null && !StringUtil::isASCII($comment)) {
                 $this->enableUtf8Name(true);
             }
         }
@@ -1153,6 +1244,8 @@ class ZipEntry
      * @param int $crc
      *
      * @return ZipEntry
+     *
+     * @internal
      */
     public function setCrc($crc)
     {
@@ -1180,15 +1273,19 @@ class ZipEntry
     public function setPassword($password, $encryptionMethod = null)
     {
         if (!$this->isDirectory) {
-            if ($encryptionMethod !== null) {
-                $this->setEncryptionMethod($encryptionMethod);
-            }
-
             if ($password === null || $password === '') {
                 $this->password = null;
                 $this->disableEncryption();
             } else {
                 $this->password = (string) $password;
+
+                if ($encryptionMethod === null && $this->encryptionMethod === ZipEncryptionMethod::NONE) {
+                    $encryptionMethod = ZipEncryptionMethod::WINZIP_AES_256;
+                }
+
+                if ($encryptionMethod !== null) {
+                    $this->setEncryptionMethod($encryptionMethod);
+                }
                 $this->setEncrypted(true);
             }
         }
@@ -1228,6 +1325,7 @@ class ZipEntry
         $this->encryptionMethod = $encryptionMethod;
 
         $this->setEncrypted($this->encryptionMethod !== ZipEncryptionMethod::NONE);
+        $this->extractVersion = self::UNKNOWN;
 
         return $this;
     }
@@ -1329,18 +1427,21 @@ class ZipEntry
      */
     public function getUnixMode()
     {
-        /** @var AsiExtraField|null $asiExtraField */
-        $asiExtraField = $this->getExtraField(AsiExtraField::HEADER_ID);
+        $mode = 0;
 
-        if ($asiExtraField !== null) {
-            return $asiExtraField->getMode();
+        if ($this->createdOS === ZipPlatform::OS_UNIX) {
+            $mode = ($this->externalAttributes >> 16) & 0xFFFF;
+        } elseif ($this->hasExtraField(AsiExtraField::HEADER_ID)) {
+            /** @var AsiExtraField $asiExtraField */
+            $asiExtraField = $this->getExtraField(AsiExtraField::HEADER_ID);
+            $mode = $asiExtraField->getMode();
         }
 
-        if ($this->createdOS === ZipPlatform::OS_UNIX) {
-            return ($this->externalAttributes >> 16) & 0xFFFF;
+        if ($mode > 0) {
+            return $mode;
         }
 
-        return $this->isDirectory ? 040755 : 0100664;
+        return $this->isDirectory ? 040755 : 0100644;
     }
 
     /**
@@ -1351,8 +1452,8 @@ class ZipEntry
      */
     public function isZip64ExtensionsRequired()
     {
-        return $this->compressedSize >= ZipConstants::ZIP64_MAGIC
-            || $this->uncompressedSize >= ZipConstants::ZIP64_MAGIC;
+        return $this->compressedSize > ZipConstants::ZIP64_MAGIC
+            || $this->uncompressedSize > ZipConstants::ZIP64_MAGIC;
     }
 
     /**

+ 37 - 18
src/Util/DateTimeConverter.php

@@ -5,6 +5,21 @@ namespace PhpZip\Util;
 /**
  * Convert unix timestamp values to DOS date/time values and vice versa.
  *
+ * The DOS date/time format is a bitmask:
+ *
+ * 24                16                 8                 0
+ * +-+-+-+-+-+-+-+-+ +-+-+-+-+-+-+-+-+ +-+-+-+-+-+-+-+-+ +-+-+-+-+-+-+-+-+
+ * |Y|Y|Y|Y|Y|Y|Y|M| |M|M|M|D|D|D|D|D| |h|h|h|h|h|m|m|m| |m|m|m|s|s|s|s|s|
+ * +-+-+-+-+-+-+-+-+ +-+-+-+-+-+-+-+-+ +-+-+-+-+-+-+-+-+ +-+-+-+-+-+-+-+-+
+ * \___________/\________/\_________/ \________/\____________/\_________/
+ * year        month       day      hour       minute        second
+ *
+ * The year is stored as an offset from 1980.
+ * Seconds are stored in two-second increments.
+ * (So if the "second" value is 15, it actually represents 30 seconds.)
+ *
+ * @see https://docs.microsoft.com/ru-ru/windows/win32/api/winbase/nf-winbase-filetimetodosdatetime?redirectedfrom=MSDN
+ *
  * @author Ne-Lexa alexey@nelexa.ru
  * @license MIT
  *
@@ -31,21 +46,21 @@ class DateTimeConverter
      *
      * @return int Unix timestamp
      */
-    public static function toUnixTimestamp($dosTime)
+    public static function msDosToUnix($dosTime)
     {
-        if ($dosTime < self::MIN_DOS_TIME) {
-            $dosTime = self::MIN_DOS_TIME;
+        if ($dosTime <= self::MIN_DOS_TIME) {
+            $dosTime = 0;
         } elseif ($dosTime > self::MAX_DOS_TIME) {
             $dosTime = self::MAX_DOS_TIME;
         }
-
+//        date_default_timezone_set('UTC');
         return mktime(
-            ($dosTime >> 11) & 0x1f,         // hour
-            ($dosTime >> 5) & 0x3f,        // minute
-            2 * ($dosTime & 0x1f),         // second
-            ($dosTime >> 21) & 0x0f,       // month
-            ($dosTime >> 16) & 0x1f,         // day
-            1980 + (($dosTime >> 25) & 0x7f) // year
+            (($dosTime >> 11) & 0x1f),         // hours
+            (($dosTime >> 5) & 0x3f),          // minutes
+            (($dosTime << 1) & 0x3e),          // seconds
+            (($dosTime >> 21) & 0x0f),         // month
+            (($dosTime >> 16) & 0x1f),         // day
+            ((($dosTime >> 25) & 0x7f) + 1980) // year
         );
     }
 
@@ -59,22 +74,26 @@ class DateTimeConverter
      *             rounded down to even seconds
      *             and is in between DateTimeConverter::MIN_DOS_TIME and DateTimeConverter::MAX_DOS_TIME
      */
-    public static function toDosTime($unixTimestamp)
+    public static function unixToMsDos($unixTimestamp)
     {
         if ($unixTimestamp < 0) {
             throw new \InvalidArgumentException('Negative unix timestamp: ' . $unixTimestamp);
         }
 
         $date = getdate($unixTimestamp);
+        $dosTime = (
+            (($date['year'] - 1980) << 25) |
+            ($date['mon'] << 21) |
+            ($date['mday'] << 16) |
+            ($date['hours'] << 11) |
+            ($date['minutes'] << 5) |
+            ($date['seconds'] >> 1)
+        );
 
-        if ($date['year'] < 1980) {
-            return self::MIN_DOS_TIME;
+        if ($dosTime <= self::MIN_DOS_TIME) {
+            $dosTime = 0;
         }
 
-        $date['year'] -= 1980;
-
-        return $date['year'] << 25 | $date['mon'] << 21 |
-            $date['mday'] << 16 | $date['hours'] << 11 |
-            $date['minutes'] << 5 | $date['seconds'] >> 1;
+        return $dosTime;
     }
 }

+ 7 - 13
src/Util/StringUtil.php

@@ -9,17 +9,6 @@ namespace PhpZip\Util;
  */
 final class StringUtil
 {
-    /**
-     * @param string $haystack
-     * @param string $needle
-     *
-     * @return bool
-     */
-    public static function startsWith($haystack, $needle)
-    {
-        return $needle === '' || strrpos($haystack, $needle, -\strlen($haystack)) !== false;
-    }
-
     /**
      * @param string $haystack
      * @param string $needle
@@ -28,8 +17,13 @@ final class StringUtil
      */
     public static function endsWith($haystack, $needle)
     {
-        return $needle === '' || (($temp = \strlen($haystack) - \strlen($needle)) >= 0
-                && strpos($haystack, $needle, $temp) !== false);
+        $length = \strlen($needle);
+
+        if ($length === 0) {
+            return true;
+        }
+
+        return substr($haystack, -$length) === $needle;
     }
 
     /**

+ 3 - 3
src/ZipFile.php

@@ -570,7 +570,7 @@ class ZipFile implements ZipFileInterface
         $zipEntry->setCompressionMethod($compressionMethod);
         $zipEntry->setCreatedOS(ZipPlatform::OS_UNIX);
         $zipEntry->setExtractedOS(ZipPlatform::OS_UNIX);
-        $zipEntry->setUnixMode(010644);
+        $zipEntry->setUnixMode(0100644);
         $zipEntry->setTime(time());
 
         $this->addZipEntry($zipEntry);
@@ -794,7 +794,7 @@ class ZipFile implements ZipFileInterface
         $zipEntry = new ZipEntry($entryName);
 
         if ($fstat !== false) {
-            $unixMode = (int) sprintf('%o', $fstat['mode']);
+            $unixMode = $fstat['mode'];
             $length = $fstat['size'];
 
             if ($compressionMethod === null || $compressionMethod === ZipEntry::UNKNOWN) {
@@ -812,7 +812,7 @@ class ZipFile implements ZipFileInterface
                 $zipEntry->setUncompressedSize($length);
             }
         } else {
-            $unixMode = 010644;
+            $unixMode = 0100644;
 
             if ($compressionMethod === null || $compressionMethod === ZipEntry::UNKNOWN) {
                 $compressionMethod = ZipCompressionMethod::DEFLATED;

+ 0 - 2
tests/SlowTests/Zip64Test.php

@@ -17,8 +17,6 @@ class Zip64Test extends ZipTestCase
 {
     /**
      * @throws ZipException
-     *
-     * @runInSeparateProcess
      */
     public function testCreateLargeZip64File()
     {

+ 6 - 0
tests/Zip64Test.php

@@ -22,6 +22,12 @@ class Zip64Test extends ZipTestCase
      */
     public function testOver65535FilesInZip()
     {
+        if (\PHP_INT_SIZE === 4) { // php 32 bit
+            static::markTestSkipped('Only php-64 bit.');
+
+            return;
+        }
+
         $countFiles = 0xffff + 1;
 
         $zipFile = new ZipFile();

+ 1561 - 0
tests/ZipEntryTest.php

@@ -0,0 +1,1561 @@
+<?php
+
+namespace PhpZip\Tests;
+
+use PHPUnit\Framework\TestCase;
+use PhpZip\Constants\DosAttrs;
+use PhpZip\Constants\DosCodePage;
+use PhpZip\Constants\GeneralPurposeBitFlag;
+use PhpZip\Constants\ZipCompressionLevel;
+use PhpZip\Constants\ZipCompressionMethod;
+use PhpZip\Constants\ZipConstants;
+use PhpZip\Constants\ZipEncryptionMethod;
+use PhpZip\Constants\ZipPlatform;
+use PhpZip\Constants\ZipVersion;
+use PhpZip\Exception\InvalidArgumentException;
+use PhpZip\Exception\ZipException;
+use PhpZip\Exception\ZipUnsupportMethodException;
+use PhpZip\Model\Data\ZipFileData;
+use PhpZip\Model\Data\ZipNewData;
+use PhpZip\Model\Extra\ExtraFieldsCollection;
+use PhpZip\Model\Extra\Fields\AsiExtraField;
+use PhpZip\Model\Extra\Fields\ExtendedTimestampExtraField;
+use PhpZip\Model\Extra\Fields\JarMarkerExtraField;
+use PhpZip\Model\Extra\Fields\NewUnixExtraField;
+use PhpZip\Model\Extra\Fields\NtfsExtraField;
+use PhpZip\Model\Extra\Fields\OldUnixExtraField;
+use PhpZip\Model\Extra\Fields\UnicodePathExtraField;
+use PhpZip\Model\ZipEntry;
+
+/**
+ * Class ZipEntryTest.
+ *
+ * @internal
+ *
+ * @small
+ */
+class ZipEntryTest extends TestCase
+{
+    public function testEntry()
+    {
+        $zipEntry = new ZipEntry('entry');
+        static::assertSame($zipEntry->getName(), 'entry');
+        static::assertFalse($zipEntry->isDirectory());
+        static::assertNull($zipEntry->getData());
+        static::assertSame($zipEntry->getCompressionMethod(), ZipEntry::UNKNOWN);
+        static::assertSame($zipEntry->getCreatedOS(), ZipEntry::UNKNOWN);
+        static::assertSame($zipEntry->getExtractedOS(), ZipEntry::UNKNOWN);
+        static::assertSame($zipEntry->getSoftwareVersion(), ZipVersion::v10_DEFAULT_MIN);
+        static::assertSame($zipEntry->getExtractVersion(), ZipVersion::v10_DEFAULT_MIN);
+        static::assertSame($zipEntry->getGeneralPurposeBitFlags(), 0);
+        static::assertSame($zipEntry->getDosTime(), ZipEntry::UNKNOWN);
+        static::assertSame($zipEntry->getTime(), ZipEntry::UNKNOWN);
+        static::assertSame($zipEntry->getCrc(), ZipEntry::UNKNOWN);
+        static::assertSame($zipEntry->getCompressedSize(), ZipEntry::UNKNOWN);
+        static::assertSame($zipEntry->getUncompressedSize(), ZipEntry::UNKNOWN);
+        static::assertSame($zipEntry->getInternalAttributes(), 0);
+        static::assertSame($zipEntry->getExternalAttributes(), DosAttrs::DOS_ARCHIVE);
+        static::assertSame($zipEntry->getLocalHeaderOffset(), 0);
+        static::assertInstanceOf(ExtraFieldsCollection::class, $zipEntry->getCdExtraFields());
+        static::assertInstanceOf(ExtraFieldsCollection::class, $zipEntry->getLocalExtraFields());
+        static::assertCount(0, $zipEntry->getCdExtraFields());
+        static::assertCount(0, $zipEntry->getLocalExtraFields());
+        static::assertSame($zipEntry->getComment(), '');
+        static::assertNull($zipEntry->getPassword());
+        static::assertSame($zipEntry->getEncryptionMethod(), ZipEncryptionMethod::NONE);
+        static::assertSame($zipEntry->getCompressionLevel(), ZipCompressionLevel::NORMAL);
+        static::assertNull($zipEntry->getCharset());
+        static::assertNull($zipEntry->getATime());
+        static::assertNull($zipEntry->getCTime());
+        static::assertSame($zipEntry->getUnixMode(), 0100644);
+
+        $zipDirEntry = $zipEntry->rename('directory/');
+        static::assertNotSame($zipEntry, $zipDirEntry);
+        static::assertSame($zipDirEntry->getName(), 'directory/');
+        static::assertTrue($zipDirEntry->isDirectory());
+        static::assertSame($zipDirEntry->getExternalAttributes(), DosAttrs::DOS_DIRECTORY);
+        static::assertSame($zipDirEntry->getUnixMode(), 040755);
+        static::assertNotSame($zipDirEntry->getName(), $zipEntry->getName());
+        static::assertNotSame($zipDirEntry->isDirectory(), $zipEntry->isDirectory());
+        static::assertNotSame($zipDirEntry->getExternalAttributes(), $zipEntry->getExternalAttributes());
+        static::assertNotSame($zipDirEntry->getUnixMode(), $zipEntry->getUnixMode());
+    }
+
+    /**
+     * @dataProvider provideEmptyName
+     *
+     * @param string|null $entryName
+     * @param string      $exceptionMessage
+     */
+    public function testEmptyName($entryName, $exceptionMessage)
+    {
+        $this->setExpectedException(InvalidArgumentException::class, $exceptionMessage);
+
+        new ZipEntry($entryName);
+    }
+
+    /**
+     * @return array
+     */
+    public function provideEmptyName()
+    {
+        return [
+            ['', 'Empty zip entry name'],
+            ['/', 'Empty zip entry name'],
+            [null, 'zip entry name is null'],
+        ];
+    }
+
+    /**
+     * @dataProvider provideEntryName
+     *
+     * @param string $entryName
+     * @param string $actualEntryName
+     * @param bool   $directory
+     */
+    public function testEntryName($entryName, $actualEntryName, $directory)
+    {
+        $entry = new ZipEntry($entryName);
+        static::assertSame($entry->getName(), $actualEntryName);
+        static::assertSame($entry->isDirectory(), $directory);
+    }
+
+    /**
+     * @return array
+     */
+    public function provideEntryName()
+    {
+        return [
+            ['0', '0', false],
+            [0, '0', false],
+            ['directory/', 'directory/', true],
+        ];
+    }
+
+    /**
+     * @dataProvider provideCompressionMethod
+     *
+     * @param int $compressionMethod
+     *
+     * @throws ZipUnsupportMethodException
+     */
+    public function testCompressionMethod($compressionMethod)
+    {
+        $entry = new ZipEntry('entry');
+        static::assertSame($entry->getCompressionMethod(), ZipEntry::UNKNOWN);
+
+        $entry->setCompressionMethod($compressionMethod);
+        static::assertSame($entry->getCompressionMethod(), $compressionMethod);
+    }
+
+    /**
+     * @return array
+     */
+    public function provideCompressionMethod()
+    {
+        $provides = [
+            [ZipCompressionMethod::STORED],
+            [ZipCompressionMethod::DEFLATED],
+        ];
+
+        if (\extension_loaded('bz2')) {
+            $provides[] = [ZipCompressionMethod::BZIP2];
+        }
+
+        return $provides;
+    }
+
+    /**
+     * @dataProvider provideOutOfRangeCompressionMethod
+     *
+     * @param int $compressionMethod
+     *
+     * @throws ZipUnsupportMethodException
+     */
+    public function testOutOfRangeCompressionMethod($compressionMethod)
+    {
+        $this->setExpectedException(InvalidArgumentException::class, 'method out of range: ' . $compressionMethod);
+
+        $zipEntry = new ZipEntry('entry');
+        $zipEntry->setCompressionMethod($compressionMethod);
+    }
+
+    /**
+     * @return array
+     */
+    public function provideOutOfRangeCompressionMethod()
+    {
+        return [
+            [-1],
+            [0x44444],
+        ];
+    }
+
+    /**
+     * @dataProvider provideUnsupportCompressionMethod
+     *
+     * @param int    $compressionMethod
+     * @param string $exceptionMessage
+     *
+     * @throws ZipUnsupportMethodException
+     */
+    public function testUnsupportCompressionMethod($compressionMethod, $exceptionMessage)
+    {
+        $this->setExpectedException(ZipUnsupportMethodException::class, $exceptionMessage);
+
+        $zipEntry = new ZipEntry('entry');
+        $zipEntry->setCompressionMethod($compressionMethod);
+    }
+
+    /**
+     * @return array
+     */
+    public function provideUnsupportCompressionMethod()
+    {
+        return [
+            [1, 'Compression method 1 (Shrunk) is not supported.'],
+            [2, 'Compression method 2 (Reduced compression factor 1) is not supported.'],
+            [3, 'Compression method 3 (Reduced compression factor 2) is not supported.'],
+            [4, 'Compression method 4 (Reduced compression factor 3) is not supported.'],
+            [5, 'Compression method 5 (Reduced compression factor 4) is not supported.'],
+            [6, 'Compression method 6 (Imploded) is not supported.'],
+            [7, 'Compression method 7 (Reserved for Tokenizing compression algorithm) is not supported.'],
+            [9, 'Compression method 9 (Enhanced Deflating using Deflate64(tm)) is not supported.'],
+            [10, 'Compression method 10 (PKWARE Data Compression Library Imploding) is not supported.'],
+            [11, 'Compression method 11 (Reserved by PKWARE) is not supported.'],
+            [13, 'Compression method 13 (Reserved by PKWARE) is not supported.'],
+            [14, 'Compression method 14 (LZMA) is not supported.'],
+            [15, 'Compression method 15 (Reserved by PKWARE) is not supported.'],
+            [16, 'Compression method 16 (Reserved by PKWARE) is not supported.'],
+            [17, 'Compression method 17 (Reserved by PKWARE) is not supported.'],
+            [18, 'Compression method 18 (File is compressed using IBM TERSE (new)) is not supported.'],
+            [19, 'Compression method 19 (IBM LZ77 z Architecture (PFS)) is not supported.'],
+            [96, 'Compression method 96 (WinZip JPEG Compression) is not supported.'],
+            [97, 'Compression method 97 (WavPack compressed data) is not supported.'],
+            [98, 'Compression method 98 (PPMd version I, Rev 1) is not supported.'],
+            [
+                ZipCompressionMethod::WINZIP_AES,
+                'Compression method ' . ZipCompressionMethod::WINZIP_AES . ' (AES Encryption) is not supported.',
+            ],
+            [100, 'Compression method 100 (Unknown Method) is not supported.'],
+        ];
+    }
+
+    public function testCharset()
+    {
+        $zipEntry = new ZipEntry('entry');
+        $zipEntry->setCharset(DosCodePage::CP_CYRILLIC_RUSSIAN);
+        static::assertSame($zipEntry->getCharset(), DosCodePage::CP_CYRILLIC_RUSSIAN);
+
+        $zipEntry->setCharset(null);
+        static::assertNull($zipEntry->getCharset());
+    }
+
+    public function testEmptyCharset()
+    {
+        $this->setExpectedException(InvalidArgumentException::class, 'Empty charset');
+
+        $zipEntry = new ZipEntry('entry');
+        $zipEntry->setCharset('');
+    }
+
+    public function testRenameAndDeleteUnicodePath()
+    {
+        $entryName = 'файл.txt';
+        $charset = DosCodePage::CP_CYRILLIC_RUSSIAN;
+        $dosEntryName = DosCodePage::fromUTF8($entryName, $charset);
+        static::assertSame(DosCodePage::toUTF8($dosEntryName, $charset), $entryName);
+
+        $unicodePathExtraField = UnicodePathExtraField::create($entryName);
+
+        $zipEntry = new ZipEntry($dosEntryName, $charset);
+        static::assertSame($zipEntry->getName(), $dosEntryName);
+        static::assertSame($zipEntry->getCharset(), $charset);
+        static::assertFalse($zipEntry->isUtf8Flag());
+        $zipEntry->addExtraField($unicodePathExtraField);
+        static::assertSame(
+            $zipEntry->getLocalExtraField(UnicodePathExtraField::HEADER_ID),
+            $unicodePathExtraField
+        );
+        static::assertSame(
+            $zipEntry->getCdExtraField(UnicodePathExtraField::HEADER_ID),
+            $unicodePathExtraField
+        );
+
+        $utf8EntryName = $zipEntry->rename($entryName);
+        static::assertSame($utf8EntryName->getName(), $entryName);
+        static::assertTrue($utf8EntryName->isUtf8Flag());
+        static::assertNull($utf8EntryName->getCharset());
+        static::assertNull($utf8EntryName->getLocalExtraField(UnicodePathExtraField::HEADER_ID));
+        static::assertNull($utf8EntryName->getCdExtraField(UnicodePathExtraField::HEADER_ID));
+    }
+
+    public function testData()
+    {
+        $zipEntry = new ZipEntry('entry');
+        static::assertNull($zipEntry->getData());
+
+        $zipData = new ZipNewData($zipEntry, 'Text contents');
+
+        $zipEntry->setData($zipData);
+        static::assertSame($zipEntry->getData(), $zipData);
+
+        $zipEntry->setData(null);
+        static::assertNull($zipEntry->getData());
+    }
+
+    /**
+     * @dataProvider providePlatform
+     *
+     * @param int $zipOS
+     */
+    public function testCreatedOS($zipOS)
+    {
+        $zipEntry = new ZipEntry('entry');
+        static::assertSame($zipEntry->getCreatedOS(), ZipEntry::UNKNOWN);
+        $zipEntry->setCreatedOS($zipOS);
+        static::assertSame($zipEntry->getCreatedOS(), $zipOS);
+    }
+
+    /**
+     * @return array
+     */
+    public function providePlatform()
+    {
+        return [
+            [ZipPlatform::OS_DOS],
+            [ZipPlatform::OS_UNIX],
+            [ZipPlatform::OS_MAC_OSX],
+        ];
+    }
+
+    /**
+     * @dataProvider providePlatform
+     *
+     * @param int $zipOS
+     */
+    public function testExtractedOS($zipOS)
+    {
+        $zipEntry = new ZipEntry('entry');
+        static::assertSame($zipEntry->getExtractedOS(), ZipEntry::UNKNOWN);
+        $zipEntry->setExtractedOS($zipOS);
+        static::assertSame($zipEntry->getExtractedOS(), $zipOS);
+    }
+
+    /**
+     * @dataProvider provideInvalidPlatform
+     *
+     * @param int $zipOS
+     */
+    public function testInvalidCreatedOs($zipOS)
+    {
+        $this->setExpectedException(InvalidArgumentException::class, 'Platform out of range');
+
+        $zipEntry = new ZipEntry('entry');
+        $zipEntry->setCreatedOS($zipOS);
+    }
+
+    /**
+     * @return array
+     */
+    public function provideInvalidPlatform()
+    {
+        return [
+            [-1],
+            [0xff + 1],
+        ];
+    }
+
+    /**
+     * @dataProvider provideInvalidPlatform
+     *
+     * @param int $zipOS
+     */
+    public function testInvalidExtractedOs($zipOS)
+    {
+        $this->setExpectedException(InvalidArgumentException::class, 'Platform out of range');
+
+        $zipEntry = new ZipEntry('entry');
+        $zipEntry->setExtractedOS($zipOS);
+    }
+
+    /**
+     * @throws ZipException
+     */
+    public function testAutoExtractVersion()
+    {
+        $zipEntry = new ZipEntry('entry');
+        static::assertSame($zipEntry->getExtractVersion(), ZipVersion::v10_DEFAULT_MIN);
+
+        $zipEntry->setCompressionMethod(ZipCompressionMethod::DEFLATED);
+        static::assertSame($zipEntry->getExtractVersion(), ZipVersion::v20_DEFLATED_FOLDER_ZIPCRYPTO);
+
+        static::assertSame(
+            (new ZipEntry('directory/'))->getExtractVersion(),
+            ZipVersion::v20_DEFLATED_FOLDER_ZIPCRYPTO
+        );
+
+        if (\extension_loaded('bz2')) {
+            $zipEntry->setCompressionMethod(ZipCompressionMethod::BZIP2);
+            static::assertSame($zipEntry->getExtractVersion(), ZipVersion::v46_BZIP2);
+        }
+
+        $zipEntry->setCompressionMethod(ZipCompressionMethod::STORED);
+        static::assertSame($zipEntry->getExtractVersion(), ZipVersion::v10_DEFAULT_MIN);
+
+        $zipEntry->setPassword('12345', ZipEncryptionMethod::PKWARE);
+        static::assertSame($zipEntry->getExtractVersion(), ZipVersion::v20_DEFLATED_FOLDER_ZIPCRYPTO);
+
+        $zipEntry->setEncryptionMethod(ZipEncryptionMethod::WINZIP_AES_256);
+        static::assertSame($zipEntry->getExtractVersion(), ZipVersion::v51_ENCR_AES_RC2_CORRECT);
+    }
+
+    /**
+     * @throws ZipException
+     */
+    public function testExtractVersion()
+    {
+        $zipEntry = new ZipEntry('entry');
+        static::assertSame($zipEntry->getExtractVersion(), ZipVersion::v10_DEFAULT_MIN);
+
+        $zipEntry->setExtractVersion(ZipVersion::v63_LZMA_PPMD_BLOWFISH_TWOFISH);
+        static::assertSame($zipEntry->getExtractVersion(), ZipVersion::v63_LZMA_PPMD_BLOWFISH_TWOFISH);
+
+        $renameEntry = $zipEntry->rename('new_entry');
+        static::assertSame($renameEntry->getExtractVersion(), ZipVersion::v63_LZMA_PPMD_BLOWFISH_TWOFISH);
+
+        $renameDirEntry = $zipEntry->rename('new_directory/');
+        static::assertSame($renameDirEntry->getExtractVersion(), ZipVersion::v63_LZMA_PPMD_BLOWFISH_TWOFISH);
+
+        $zipEntry->setExtractVersion(ZipVersion::v10_DEFAULT_MIN);
+        static::assertSame($zipEntry->getExtractVersion(), ZipVersion::v10_DEFAULT_MIN);
+
+        $renameDirEntry = $zipEntry->rename('new_directory/');
+        static::assertSame($renameDirEntry->getExtractVersion(), ZipVersion::v20_DEFLATED_FOLDER_ZIPCRYPTO);
+
+        $zipEntry->setCompressionMethod(ZipCompressionMethod::DEFLATED);
+        static::assertSame($zipEntry->getExtractVersion(), ZipVersion::v20_DEFLATED_FOLDER_ZIPCRYPTO);
+
+        if (\extension_loaded('bz2')) {
+            $zipEntry->setExtractVersion(ZipVersion::v10_DEFAULT_MIN);
+            $zipEntry->setCompressionMethod(ZipCompressionMethod::BZIP2);
+            static::assertSame($zipEntry->getExtractVersion(), ZipVersion::v46_BZIP2);
+        }
+
+        $zipEntry->setExtractVersion(ZipVersion::v63_LZMA_PPMD_BLOWFISH_TWOFISH);
+        $zipEntry->setCompressionMethod(ZipCompressionMethod::STORED);
+        static::assertSame($zipEntry->getExtractVersion(), ZipVersion::v10_DEFAULT_MIN);
+
+        $zipEntry->setExtractVersion(ZipVersion::v10_DEFAULT_MIN);
+        $zipEntry->setPassword('12345', ZipEncryptionMethod::PKWARE);
+        static::assertSame($zipEntry->getExtractVersion(), ZipVersion::v20_DEFLATED_FOLDER_ZIPCRYPTO);
+
+        $zipEntry->setExtractVersion(ZipVersion::v10_DEFAULT_MIN);
+        $zipEntry->setEncryptionMethod(ZipEncryptionMethod::WINZIP_AES_256);
+        static::assertSame($zipEntry->getExtractVersion(), ZipVersion::v51_ENCR_AES_RC2_CORRECT);
+    }
+
+    public function testSoftwareVersion()
+    {
+        $zipEntry = new ZipEntry('entry');
+        static::assertSame($zipEntry->getSoftwareVersion(), $zipEntry->getExtractVersion());
+
+        $zipEntry->setExtractVersion(ZipVersion::v45_ZIP64_EXT);
+        static::assertSame($zipEntry->getSoftwareVersion(), $zipEntry->getExtractVersion());
+
+        $softwareVersion = 35;
+        $zipEntry->setSoftwareVersion($softwareVersion);
+        static::assertSame($softwareVersion, $zipEntry->getSoftwareVersion());
+        static::assertSame($zipEntry->getExtractVersion(), ZipVersion::v45_ZIP64_EXT);
+
+        $zipEntry->setExtractVersion(ZipVersion::v63_LZMA_PPMD_BLOWFISH_TWOFISH);
+        static::assertNotSame($zipEntry->getSoftwareVersion(), $zipEntry->getExtractVersion());
+        static::assertSame($softwareVersion, $zipEntry->getSoftwareVersion());
+        static::assertSame($zipEntry->getExtractVersion(), ZipVersion::v63_LZMA_PPMD_BLOWFISH_TWOFISH);
+    }
+
+    public function testSize()
+    {
+        $zipEntry = new ZipEntry('entry');
+        static::assertSame($zipEntry->getCompressedSize(), ZipEntry::UNKNOWN);
+        static::assertSame($zipEntry->getUncompressedSize(), ZipEntry::UNKNOWN);
+
+        $compressedSize = 100000;
+        $uncompressedSize = 400000;
+
+        $zipEntry->setCompressedSize($compressedSize);
+        $zipEntry->setUncompressedSize($uncompressedSize);
+        static::assertSame($zipEntry->getCompressedSize(), $compressedSize);
+        static::assertSame($zipEntry->getUncompressedSize(), $uncompressedSize);
+
+        $zipEntry->setCompressedSize(ZipEntry::UNKNOWN);
+        $zipEntry->setUncompressedSize(ZipEntry::UNKNOWN);
+        static::assertSame($zipEntry->getCompressedSize(), ZipEntry::UNKNOWN);
+        static::assertSame($zipEntry->getUncompressedSize(), ZipEntry::UNKNOWN);
+    }
+
+    public function testInvalidCompressedSize()
+    {
+        $this->setExpectedException(InvalidArgumentException::class, 'Compressed size < -1');
+
+        $zipEntry = new ZipEntry('entry');
+        $zipEntry->setCompressedSize(-2);
+    }
+
+    public function testInvalidUncompressedSize()
+    {
+        $this->setExpectedException(InvalidArgumentException::class, 'Uncompressed size < -1');
+
+        $zipEntry = new ZipEntry('entry');
+        $zipEntry->setUncompressedSize(-2);
+    }
+
+    public function testLocalHeaderOffset()
+    {
+        $zipEntry = new ZipEntry('entry');
+        static::assertSame($zipEntry->getLocalHeaderOffset(), 0);
+
+        $localHeaderOffset = 10000;
+        $zipEntry->setLocalHeaderOffset($localHeaderOffset);
+        static::assertSame($zipEntry->getLocalHeaderOffset(), $localHeaderOffset);
+
+        $this->setExpectedException(InvalidArgumentException::class, 'Negative $localHeaderOffset');
+        $zipEntry->setLocalHeaderOffset(-1);
+    }
+
+    public function testGeneralPurposeBitFlags()
+    {
+        $zipEntry = new ZipEntry('entry');
+        static::assertSame($zipEntry->getGeneralPurposeBitFlags(), 0);
+        static::assertFalse($zipEntry->isUtf8Flag());
+        static::assertFalse($zipEntry->isEncrypted());
+        static::assertFalse($zipEntry->isStrongEncryption());
+        static::assertFalse($zipEntry->isDataDescriptorEnabled());
+
+        $gpbf = GeneralPurposeBitFlag::DATA_DESCRIPTOR | GeneralPurposeBitFlag::UTF8;
+        $zipEntry->setGeneralPurposeBitFlags($gpbf);
+        static::assertSame($zipEntry->getGeneralPurposeBitFlags(), $gpbf);
+        static::assertTrue($zipEntry->isDataDescriptorEnabled());
+        static::assertTrue($zipEntry->isUtf8Flag());
+
+        $zipEntry->setGeneralPurposeBitFlags(0);
+        static::assertSame($zipEntry->getGeneralPurposeBitFlags(), 0);
+        static::assertFalse($zipEntry->isUtf8Flag());
+        static::assertFalse($zipEntry->isDataDescriptorEnabled());
+
+        $zipEntry->enableUtf8Name(true);
+        static::assertTrue($zipEntry->isUtf8Flag());
+        static::assertSame(
+            ($zipEntry->getGeneralPurposeBitFlags() & GeneralPurposeBitFlag::UTF8),
+            GeneralPurposeBitFlag::UTF8
+        );
+        $zipEntry->enableUtf8Name(false);
+        static::assertFalse($zipEntry->isUtf8Flag());
+        static::assertSame(
+            ($zipEntry->getGeneralPurposeBitFlags() & GeneralPurposeBitFlag::UTF8),
+            0
+        );
+
+        $zipEntry->enableDataDescriptor(true);
+        static::assertTrue($zipEntry->isDataDescriptorEnabled());
+        static::assertSame(
+            ($zipEntry->getGeneralPurposeBitFlags() & GeneralPurposeBitFlag::DATA_DESCRIPTOR),
+            GeneralPurposeBitFlag::DATA_DESCRIPTOR
+        );
+        $zipEntry->enableDataDescriptor(false);
+        static::assertFalse($zipEntry->isDataDescriptorEnabled());
+        static::assertSame(
+            ($zipEntry->getGeneralPurposeBitFlags() & GeneralPurposeBitFlag::DATA_DESCRIPTOR),
+            0
+        );
+    }
+
+    public function testEncryptionGPBF()
+    {
+        $zipEntry = new ZipEntry('entry');
+        static::assertFalse($zipEntry->isEncrypted());
+
+        $zipEntry->setGeneralPurposeBitFlags(GeneralPurposeBitFlag::ENCRYPTION);
+
+        static::assertSame(
+            ($zipEntry->getGeneralPurposeBitFlags() & GeneralPurposeBitFlag::ENCRYPTION),
+            GeneralPurposeBitFlag::ENCRYPTION
+        );
+        static::assertTrue($zipEntry->isEncrypted());
+
+        $zipEntry->disableEncryption();
+        static::assertSame(
+            ($zipEntry->getGeneralPurposeBitFlags() & GeneralPurposeBitFlag::ENCRYPTION),
+            0
+        );
+        static::assertFalse($zipEntry->isEncrypted());
+
+        // SIC! Strong encryption is not supported in ZipReader and ZipWriter
+        static::assertFalse($zipEntry->isStrongEncryption());
+        $zipEntry->setGeneralPurposeBitFlags(GeneralPurposeBitFlag::STRONG_ENCRYPTION);
+        static::assertTrue($zipEntry->isStrongEncryption());
+    }
+
+    /**
+     * @dataProvider provideInvalidGPBF
+     *
+     * @param int $gpbf
+     */
+    public function testInvalidGPBF($gpbf)
+    {
+        $this->setExpectedException(InvalidArgumentException::class, 'general purpose bit flags out of range');
+
+        $zipEntry = new ZipEntry('entry');
+        $zipEntry->setGeneralPurposeBitFlags($gpbf);
+    }
+
+    /**
+     * @return array
+     */
+    public function provideInvalidGPBF()
+    {
+        return [
+            [-1],
+            [0x10000],
+        ];
+    }
+
+    /**
+     * @dataProvider provideCompressionLevelGPBF
+     *
+     * @param int  $compressionLevel
+     * @param bool $bit1
+     * @param bool $bit2
+     *
+     * @throws ZipUnsupportMethodException
+     */
+    public function testSetCompressionFlags($compressionLevel, $bit1, $bit2)
+    {
+        $zipEntry = new ZipEntry('entry');
+        $zipEntry->setCompressionMethod(ZipCompressionMethod::DEFLATED);
+
+        $gpbf = ($bit1 ? GeneralPurposeBitFlag::COMPRESSION_FLAG1 : 0) |
+            ($bit2 ? GeneralPurposeBitFlag::COMPRESSION_FLAG2 : 0);
+        $zipEntry->setGeneralPurposeBitFlags($gpbf);
+        static::assertSame($zipEntry->getCompressionLevel(), $compressionLevel);
+
+        static::assertSame(
+            (
+                $zipEntry->getGeneralPurposeBitFlags() & GeneralPurposeBitFlag::COMPRESSION_FLAG1
+            ) === GeneralPurposeBitFlag::COMPRESSION_FLAG1,
+            $bit1,
+            'Compression flag1 is not same'
+        );
+        static::assertSame(
+            (
+                $zipEntry->getGeneralPurposeBitFlags() & GeneralPurposeBitFlag::COMPRESSION_FLAG2
+            ) === GeneralPurposeBitFlag::COMPRESSION_FLAG2,
+            $bit2,
+            'Compression flag2 is not same'
+        );
+    }
+
+    /**
+     * @return array
+     */
+    public function provideCompressionLevelGPBF()
+    {
+        return [
+            [ZipCompressionLevel::SUPER_FAST, true, true],
+            [ZipCompressionLevel::FAST, false, true],
+            [ZipCompressionLevel::NORMAL, false, false],
+            [ZipCompressionLevel::MAXIMUM, true, false],
+        ];
+    }
+
+    /**
+     * @dataProvider provideCompressionLevels
+     *
+     * @param int  $compressionLevel
+     * @param bool $bit1
+     * @param bool $bit2
+     *
+     * @throws ZipUnsupportMethodException
+     */
+    public function testSetCompressionLevel($compressionLevel, $bit1, $bit2)
+    {
+        $zipEntry = new ZipEntry('entry');
+        $zipEntry->setCompressionMethod(ZipCompressionMethod::DEFLATED);
+
+        $zipEntry->setCompressionLevel($compressionLevel);
+        static::assertSame($zipEntry->getCompressionLevel(), $compressionLevel);
+
+        static::assertSame(
+            (
+                $zipEntry->getGeneralPurposeBitFlags() & GeneralPurposeBitFlag::COMPRESSION_FLAG1
+            ) === GeneralPurposeBitFlag::COMPRESSION_FLAG1,
+            $bit1,
+            'Compression flag1 is not same'
+        );
+        static::assertSame(
+            (
+                $zipEntry->getGeneralPurposeBitFlags() & GeneralPurposeBitFlag::COMPRESSION_FLAG2
+            ) === GeneralPurposeBitFlag::COMPRESSION_FLAG2,
+            $bit2,
+            'Compression flag2 is not same'
+        );
+    }
+
+    /**
+     * @return array
+     */
+    public function provideCompressionLevels()
+    {
+        return [
+            [ZipCompressionLevel::SUPER_FAST, true, true],
+            [ZipCompressionLevel::FAST, false, true],
+            [3, false, false],
+            [4, false, false],
+            [ZipCompressionLevel::NORMAL, false, false],
+            [6, false, false],
+            [7, false, false],
+            [8, false, false],
+            [ZipCompressionLevel::MAXIMUM, true, false],
+        ];
+    }
+
+    /**
+     * @throws ZipException
+     */
+    public function testLegacyDefaultCompressionLevel()
+    {
+        $zipEntry = new ZipEntry('entry');
+        $zipEntry->setCompressionMethod(ZipCompressionMethod::DEFLATED);
+        $zipEntry->setCompressionLevel(ZipCompressionLevel::MAXIMUM);
+        static::assertSame($zipEntry->getCompressionLevel(), ZipCompressionLevel::MAXIMUM);
+
+        $zipEntry->setCompressionLevel(ZipEntry::UNKNOWN);
+        static::assertSame($zipEntry->getCompressionLevel(), ZipCompressionLevel::NORMAL);
+    }
+
+    /**
+     * @dataProvider provideInvalidCompressionLevel
+     *
+     * @param int $compressionLevel
+     *
+     * @throws ZipException
+     */
+    public function testInvalidCompressionLevel($compressionLevel)
+    {
+        $this->setExpectedException(
+            InvalidArgumentException::class,
+            'Invalid compression level. Minimum level ' . ZipCompressionLevel::LEVEL_MIN .
+            '. Maximum level ' . ZipCompressionLevel::LEVEL_MAX
+        );
+
+        $zipEntry = new ZipEntry('entry');
+        $zipEntry->setCompressionMethod(ZipCompressionMethod::DEFLATED);
+        $zipEntry->setCompressionLevel($compressionLevel);
+    }
+
+    /**
+     * @return array
+     */
+    public function provideInvalidCompressionLevel()
+    {
+        return [
+            [0],
+            [-2],
+            [10],
+            [100],
+        ];
+    }
+
+    /**
+     * @dataProvider provideDosTime
+     *
+     * @param int $dosTime
+     * @param int $timestamp
+     */
+    public function testDosTime($dosTime, $timestamp)
+    {
+        $zipEntry = new ZipEntry('entry');
+        static::assertSame($zipEntry->getDosTime(), ZipEntry::UNKNOWN);
+
+        $zipEntry->setDosTime($dosTime);
+        static::assertSame($zipEntry->getDosTime(), $dosTime);
+        static::assertSame($zipEntry->getTime(), $timestamp);
+
+        $zipEntry->setTime($timestamp);
+        static::assertSame($zipEntry->getTime(), $timestamp);
+        static::assertSame($zipEntry->getDosTime(), $dosTime);
+    }
+
+    /**
+     * @return array
+     */
+    public function provideDosTime()
+    {
+        return [
+            [0, 312757200],
+            [1043487716, 1295339468],
+            [1177556759, 1421366206],
+            [1282576076, 1521384864],
+        ];
+    }
+
+    /**
+     * @dataProvider provideInvalidDosTime
+     *
+     * @param int $dosTime
+     */
+    public function testInvalidDosTime($dosTime)
+    {
+        $this->setExpectedException(InvalidArgumentException::class, 'DosTime out of range');
+
+        $zipEntry = new ZipEntry('entry');
+        $zipEntry->setDosTime($dosTime);
+    }
+
+    /**
+     * @return array
+     */
+    public function provideInvalidDosTime()
+    {
+        return [
+            [-1],
+            [0xffffffff + 1],
+        ];
+    }
+
+    public function testSetTime()
+    {
+        $zipEntry = new ZipEntry('entry');
+        static::assertSame($zipEntry->getDosTime(), ZipEntry::UNKNOWN);
+        $zipEntry->setTime(ZipEntry::UNKNOWN);
+        static::assertSame($zipEntry->getDosTime(), 0);
+
+        $zipEntry->setTime(0);
+        static::assertSame($zipEntry->getDosTime(), 0);
+    }
+
+    /**
+     * @dataProvider provideExternalAttributes
+     *
+     * @param string   $entryName
+     * @param int      $expectedExternalAttr
+     * @param int      $createdOS
+     * @param int      $extractedOS
+     * @param int|null $externalAttr
+     * @param int      $unixMode
+     *
+     * @noinspection PhpTooManyParametersInspection
+     */
+    public function testExternalAttributes(
+        $entryName,
+        $expectedExternalAttr,
+        $createdOS,
+        $extractedOS,
+        $externalAttr,
+        $unixMode
+    ) {
+        $zipEntry = new ZipEntry($entryName);
+        static::assertSame($zipEntry->getExternalAttributes(), $expectedExternalAttr);
+        $zipEntry
+            ->setCreatedOS($createdOS)
+            ->setExtractedOS($extractedOS)
+        ;
+
+        if ($externalAttr !== null) {
+            $zipEntry->setExternalAttributes($externalAttr);
+            static::assertSame($zipEntry->getExternalAttributes(), $externalAttr);
+        }
+
+        static::assertSame($zipEntry->getUnixMode(), $unixMode);
+    }
+
+    /**
+     * @return array
+     */
+    public function provideExternalAttributes()
+    {
+        return [
+            [
+                'entry.txt',
+                DosAttrs::DOS_ARCHIVE,
+                ZipPlatform::OS_UNIX,
+                ZipPlatform::OS_UNIX,
+                (010644 << 16) | DosAttrs::DOS_ARCHIVE,
+                010644,
+            ],
+            [
+                'dir/',
+                DosAttrs::DOS_DIRECTORY,
+                ZipPlatform::OS_UNIX,
+                ZipPlatform::OS_UNIX,
+                (040755 << 16) | DosAttrs::DOS_DIRECTORY,
+                040755,
+            ],
+            [
+                'entry.txt',
+                DosAttrs::DOS_ARCHIVE,
+                ZipPlatform::OS_DOS,
+                ZipPlatform::OS_DOS,
+                null,
+                0100644,
+            ],
+            [
+                'entry.txt',
+                DosAttrs::DOS_ARCHIVE,
+                ZipPlatform::OS_DOS,
+                ZipPlatform::OS_UNIX,
+                null,
+                0100644,
+            ],
+            [
+                'entry.txt',
+                DosAttrs::DOS_ARCHIVE,
+                ZipPlatform::OS_UNIX,
+                ZipPlatform::OS_DOS,
+                null,
+                0100644,
+            ],
+            [
+                'dir/',
+                DosAttrs::DOS_DIRECTORY,
+                ZipPlatform::OS_DOS,
+                ZipPlatform::OS_DOS,
+                null,
+                040755,
+            ],
+            [
+                'dir/',
+                DosAttrs::DOS_DIRECTORY,
+                ZipPlatform::OS_DOS,
+                ZipPlatform::OS_UNIX,
+                null,
+                040755,
+            ],
+            [
+                'dir/',
+                DosAttrs::DOS_DIRECTORY,
+                ZipPlatform::OS_UNIX,
+                ZipPlatform::OS_DOS,
+                null,
+                040755,
+            ],
+            [
+                'entry.txt',
+                DosAttrs::DOS_ARCHIVE,
+                ZipPlatform::OS_UNIX,
+                ZipPlatform::OS_UNIX,
+                0777 << 16,
+                0777,
+            ],
+        ];
+    }
+
+    /**
+     * @dataProvider provideInvalidExternalAttributes
+     *
+     * @param int $externalAttributes
+     */
+    public function testInvalidExternalAttributes($externalAttributes)
+    {
+        $this->setExpectedException(InvalidArgumentException::class, 'external attributes out of range');
+
+        $zipEntry = new ZipEntry('entry');
+        $zipEntry->setExternalAttributes($externalAttributes);
+    }
+
+    /**
+     * @return array
+     */
+    public function provideInvalidExternalAttributes()
+    {
+        return [
+            [-1],
+            [0xffffffff + 1],
+        ];
+    }
+
+    public function testInternalAttributes()
+    {
+        $zipEntry = new ZipEntry('entry');
+        static::assertSame($zipEntry->getInternalAttributes(), 0);
+
+        $zipEntry->setInternalAttributes(1);
+        static::assertSame($zipEntry->getInternalAttributes(), 1);
+    }
+
+    /**
+     * @dataProvider provideInvalidInternalAttributes
+     *
+     * @param int $internalAttributes
+     */
+    public function testInvalidInternalAttributes($internalAttributes)
+    {
+        $this->setExpectedException(InvalidArgumentException::class, 'internal attributes out of range');
+
+        $zipEntry = new ZipEntry('entry');
+        $zipEntry->setInternalAttributes($internalAttributes);
+    }
+
+    /**
+     * @return array
+     */
+    public function provideInvalidInternalAttributes()
+    {
+        return [
+            [-1],
+            [0xffff + 1],
+        ];
+    }
+
+    public function testExtraFields()
+    {
+        $zipEntry = new ZipEntry('entry');
+
+        static::assertInstanceOf(ExtraFieldsCollection::class, $zipEntry->getCdExtraFields());
+        static::assertInstanceOf(ExtraFieldsCollection::class, $zipEntry->getLocalExtraFields());
+        static::assertCount(0, $zipEntry->getCdExtraFields());
+        static::assertCount(0, $zipEntry->getLocalExtraFields());
+
+        $extraNtfs = new NtfsExtraField(time(), time() - 10000, time() - 100000);
+        $extraAsi = new AsiExtraField(010644);
+        $extraJar = new JarMarkerExtraField();
+
+        $zipEntry->getLocalExtraFields()->add($extraNtfs);
+        $zipEntry->getCdExtraFields()->add($extraNtfs);
+        static::assertCount(1, $zipEntry->getCdExtraFields());
+        static::assertCount(1, $zipEntry->getLocalExtraFields());
+
+        $zipEntry->addExtraField($extraAsi);
+        static::assertCount(2, $zipEntry->getCdExtraFields());
+        static::assertCount(2, $zipEntry->getLocalExtraFields());
+
+        $zipEntry->addCdExtraField($extraJar);
+        static::assertCount(3, $zipEntry->getCdExtraFields());
+        static::assertCount(2, $zipEntry->getLocalExtraFields());
+
+        static::assertSame($zipEntry->getCdExtraField(JarMarkerExtraField::HEADER_ID), $extraJar);
+        static::assertNull($zipEntry->getLocalExtraField(JarMarkerExtraField::HEADER_ID));
+        static::assertSame($zipEntry->getLocalExtraField(AsiExtraField::HEADER_ID), $extraAsi);
+
+        static::assertSame(
+            [$extraNtfs, $extraAsi, $extraJar],
+            array_values($zipEntry->getCdExtraFields()->getAll())
+        );
+        static::assertSame(
+            [$extraNtfs, $extraAsi],
+            array_values($zipEntry->getLocalExtraFields()->getAll())
+        );
+
+        $zipEntry->removeExtraField(AsiExtraField::HEADER_ID);
+        static::assertNull($zipEntry->getCdExtraField(AsiExtraField::HEADER_ID));
+        static::assertNull($zipEntry->getLocalExtraField(AsiExtraField::HEADER_ID));
+
+        static::assertCount(2, $zipEntry->getCdExtraFields());
+        static::assertCount(1, $zipEntry->getLocalExtraFields());
+        static::assertSame(
+            [$extraNtfs, $extraJar],
+            array_values($zipEntry->getCdExtraFields()->getAll())
+        );
+        static::assertSame(
+            [$extraNtfs],
+            array_values($zipEntry->getLocalExtraFields()->getAll())
+        );
+
+        static::assertTrue($zipEntry->hasExtraField(NtfsExtraField::HEADER_ID));
+        static::assertTrue($zipEntry->hasExtraField(JarMarkerExtraField::HEADER_ID));
+        static::assertFalse($zipEntry->hasExtraField(AsiExtraField::HEADER_ID));
+    }
+
+    public function testComment()
+    {
+        $zipEntry = new ZipEntry('entry');
+        static::assertSame($zipEntry->getComment(), '');
+        $zipEntry->setComment('comment');
+        static::assertSame($zipEntry->getComment(), 'comment');
+        $zipEntry->setComment(null);
+        static::assertSame($zipEntry->getComment(), '');
+        static::assertFalse($zipEntry->isUtf8Flag());
+        $zipEntry->setComment('комментарий');
+        static::assertTrue($zipEntry->isUtf8Flag());
+        static::assertSame($zipEntry->getComment(), 'комментарий');
+    }
+
+    /**
+     * @throws \Exception
+     */
+    public function testLongComment()
+    {
+        $this->setExpectedException(InvalidArgumentException::class, 'Comment too long');
+
+        $longComment = random_bytes(0xffff + 1);
+        $zipEntry = new ZipEntry('entry');
+        $zipEntry->setComment($longComment);
+    }
+
+    /**
+     * @dataProvider provideDataDescriptorRequired
+     *
+     * @param int  $crc
+     * @param int  $compressedSize
+     * @param int  $uncompressedSize
+     * @param bool $required
+     */
+    public function testDataDescriptorRequired($crc, $compressedSize, $uncompressedSize, $required)
+    {
+        $zipEntry = new ZipEntry('entry');
+        $zipEntry->setCrc($crc);
+        $zipEntry->setCompressedSize($compressedSize);
+        $zipEntry->setUncompressedSize($uncompressedSize);
+
+        static::assertSame($zipEntry->isDataDescriptorRequired(), $required);
+        static::assertSame($zipEntry->getCrc(), $crc);
+        static::assertSame($zipEntry->getCompressedSize(), $compressedSize);
+        static::assertSame($zipEntry->getUncompressedSize(), $uncompressedSize);
+    }
+
+    /**
+     * @return array
+     */
+    public function provideDataDescriptorRequired()
+    {
+        return [
+            [ZipEntry::UNKNOWN, ZipEntry::UNKNOWN, ZipEntry::UNKNOWN, true],
+            [0xF33F33, ZipEntry::UNKNOWN, ZipEntry::UNKNOWN, true],
+            [0xF33F33, 11111111, ZipEntry::UNKNOWN, true],
+            [0xF33F33, ZipEntry::UNKNOWN, 22333333, true],
+            [ZipEntry::UNKNOWN, 11111111, ZipEntry::UNKNOWN, true],
+            [ZipEntry::UNKNOWN, 11111111, 22333333, true],
+            [ZipEntry::UNKNOWN, ZipEntry::UNKNOWN, 22333333, true],
+            [0xF33F33, 11111111, 22333333, false],
+        ];
+    }
+
+    /**
+     * @dataProvider provideEncryption
+     *
+     * @param string|null $password
+     * @param int|null    $encryptionMethod
+     * @param bool        $encrypted
+     * @param int         $expectedEncryptionMethod
+     */
+    public function testEncryption($password, $encryptionMethod, $encrypted, $expectedEncryptionMethod)
+    {
+        $zipEntry = new ZipEntry('entry');
+        $zipEntry->setPassword($password, $encryptionMethod);
+
+        static::assertSame($zipEntry->isEncrypted(), $encrypted);
+        static::assertSame($zipEntry->getPassword(), $password);
+        static::assertSame($zipEntry->getEncryptionMethod(), $expectedEncryptionMethod);
+
+        $zipEntry->setPassword($password, null);
+        static::assertSame($zipEntry->getEncryptionMethod(), $expectedEncryptionMethod);
+    }
+
+    /**
+     * @return array
+     */
+    public function provideEncryption()
+    {
+        return [
+            [null, null, false, ZipEncryptionMethod::NONE],
+            [null, ZipEncryptionMethod::WINZIP_AES_256, false, ZipEncryptionMethod::NONE],
+            ['12345', null, true, ZipEncryptionMethod::WINZIP_AES_256],
+            ['12345', ZipEncryptionMethod::PKWARE, true, ZipEncryptionMethod::PKWARE],
+            ['12345', ZipEncryptionMethod::WINZIP_AES_256, true, ZipEncryptionMethod::WINZIP_AES_256],
+            ['12345', ZipEncryptionMethod::WINZIP_AES_128, true, ZipEncryptionMethod::WINZIP_AES_128],
+            ['12345', ZipEncryptionMethod::WINZIP_AES_192, true, ZipEncryptionMethod::WINZIP_AES_192],
+        ];
+    }
+
+    public function testDirectoryEncryption()
+    {
+        $zipEntry = new ZipEntry('directory/');
+        $zipEntry->setPassword('12345', ZipEncryptionMethod::WINZIP_AES_256);
+        static::assertTrue($zipEntry->isDirectory());
+        static::assertNull($zipEntry->getPassword());
+        static::assertFalse($zipEntry->isEncrypted());
+        static::assertSame($zipEntry->getEncryptionMethod(), ZipEncryptionMethod::NONE);
+    }
+
+    /**
+     * @dataProvider provideEncryptionMethod
+     *
+     * @param int|null $encryptionMethod
+     * @param int      $expectedEncryptionMethod
+     * @param bool     $encrypted
+     * @param int      $extractVersion
+     */
+    public function testEncryptionMethod(
+        $encryptionMethod,
+        $expectedEncryptionMethod,
+        $encrypted,
+        $extractVersion
+    ) {
+        $zipEntry = new ZipEntry('entry');
+        $zipEntry->setEncryptionMethod($encryptionMethod);
+        static::assertSame($zipEntry->isEncrypted(), $encrypted);
+        static::assertSame($zipEntry->getEncryptionMethod(), $expectedEncryptionMethod);
+        static::assertSame($zipEntry->getExtractVersion(), $extractVersion);
+    }
+
+    /**
+     * @return array
+     */
+    public function provideEncryptionMethod()
+    {
+        return [
+            [
+                null,
+                ZipEncryptionMethod::NONE,
+                false,
+                ZipVersion::v10_DEFAULT_MIN,
+            ],
+            [
+                ZipEncryptionMethod::NONE,
+                ZipEncryptionMethod::NONE,
+                false,
+                ZipVersion::v10_DEFAULT_MIN,
+            ],
+            [
+                ZipEncryptionMethod::PKWARE,
+                ZipEncryptionMethod::PKWARE,
+                true,
+                ZipVersion::v20_DEFLATED_FOLDER_ZIPCRYPTO,
+            ],
+            [
+                ZipEncryptionMethod::WINZIP_AES_256,
+                ZipEncryptionMethod::WINZIP_AES_256,
+                true,
+                ZipVersion::v51_ENCR_AES_RC2_CORRECT,
+            ],
+            [
+                ZipEncryptionMethod::WINZIP_AES_192,
+                ZipEncryptionMethod::WINZIP_AES_192,
+                true,
+                ZipVersion::v51_ENCR_AES_RC2_CORRECT,
+            ],
+            [
+                ZipEncryptionMethod::WINZIP_AES_128,
+                ZipEncryptionMethod::WINZIP_AES_128,
+                true,
+                ZipVersion::v51_ENCR_AES_RC2_CORRECT,
+            ],
+        ];
+    }
+
+    /**
+     * @dataProvider provideInvalidEncryptionMethod
+     *
+     * @param int $encryptionMethod
+     */
+    public function testInvalidEncryptionMethod($encryptionMethod)
+    {
+        $this->setExpectedException(
+            InvalidArgumentException::class,
+            'Encryption method ' . $encryptionMethod . ' is not supported.'
+        );
+
+        $zipEntry = new ZipEntry('entry');
+        $zipEntry->setEncryptionMethod($encryptionMethod);
+    }
+
+    /**
+     * @return array
+     */
+    public function provideInvalidEncryptionMethod()
+    {
+        return [
+            [-2],
+            [4],
+            [5],
+        ];
+    }
+
+    /**
+     * @dataProvider provideUnixMode
+     *
+     * @param string $entryName
+     * @param int    $unixMode
+     */
+    public function testUnixMode($entryName, $unixMode)
+    {
+        $zipEntry = new ZipEntry($entryName);
+        $zipEntry->setUnixMode($unixMode);
+
+        static::assertSame($zipEntry->getUnixMode(), $unixMode);
+        static::assertSame($zipEntry->getCreatedOS(), ZipPlatform::OS_UNIX);
+    }
+
+    /**
+     * @return array
+     */
+    public function provideUnixMode()
+    {
+        return [
+            ['entry.txt', 0700], // read, write, & execute only for owner
+            ['entry.txt', 0770], // read, write, & execute for owner and group
+            ['entry.txt', 0777], // read, write, & execute for owner, group and others
+            ['entry.txt', 0111], // execute
+            ['entry.txt', 0222], // write
+            ['entry.txt', 0333], // write & execute
+            ['entry.txt', 0444], // read
+            ['entry.txt', 0555], // read & execute
+            ['entry.txt', 0666], // read & write
+            ['entry.txt', 0740], // owner can read, write, & execute; group can only read; others have no permissions
+            ['entry.txt', 0777], // owner can read, write, & execute
+            ['directory/', 040700], // directory, read, write, & execute only for owner
+            ['directory/', 040770], // directory, read, write, & execute for owner and group
+            ['directory/', 040777], // directory, read, write, & execute
+        ];
+    }
+
+    /**
+     * @dataProvider provideUnixMode
+     * @dataProvider provideSymlink
+     *
+     * @param      $entryName
+     * @param      $unixMode
+     * @param bool $symlink
+     */
+    public function testSymlink($entryName, $unixMode, $symlink = false)
+    {
+        $zipEntry = new ZipEntry($entryName);
+        $zipEntry->setUnixMode($unixMode);
+        static::assertSame($zipEntry->isUnixSymlink(), $symlink);
+    }
+
+    public function testAsiUnixMode()
+    {
+        $unixMode = 0100666;
+        $asiUnixMode = 0100600;
+
+        $asiExtraField = new AsiExtraField($asiUnixMode);
+
+        $zipEntry = new ZipEntry('entry');
+        $zipEntry->setCreatedOS(ZipPlatform::OS_DOS);
+        $zipEntry->setExtractedOS(ZipPlatform::OS_DOS);
+        $zipEntry->setExternalAttributes(DosAttrs::DOS_ARCHIVE);
+        $zipEntry->addExtraField($asiExtraField);
+
+        static::assertSame($zipEntry->getUnixMode(), $asiUnixMode);
+
+        $zipEntry->setUnixMode($unixMode);
+        static::assertSame($zipEntry->getCreatedOS(), ZipPlatform::OS_UNIX);
+        static::assertSame($zipEntry->getUnixMode(), $unixMode);
+    }
+
+    /**
+     * @return array
+     */
+    public function provideSymlink()
+    {
+        return [
+            ['entry', 0120644, true],
+            ['dir/', 0120755, true],
+        ];
+    }
+
+    /**
+     * @dataProvider provideIsZip64ExtensionsRequired
+     *
+     * @param int  $compressionSize
+     * @param int  $uncompressionSize
+     * @param bool $required
+     */
+    public function testIsZip64ExtensionsRequired($compressionSize, $uncompressionSize, $required)
+    {
+        if (\PHP_INT_SIZE === 4) {
+            static::markTestSkipped('only php 64-bit');
+
+            return;
+        }
+
+        $zipEntry = new ZipEntry('entry');
+        $zipEntry->setCompressedSize($compressionSize);
+        $zipEntry->setUncompressedSize($uncompressionSize);
+        static::assertSame($zipEntry->isZip64ExtensionsRequired(), $required);
+    }
+
+    /**
+     * @return array
+     */
+    public function provideIsZip64ExtensionsRequired()
+    {
+        return [
+            [11111111, 22222222, false],
+            [ZipEntry::UNKNOWN, ZipEntry::UNKNOWN, false],
+            [ZipEntry::UNKNOWN, ZipConstants::ZIP64_MAGIC + 1, true],
+            [ZipConstants::ZIP64_MAGIC + 1, ZipEntry::UNKNOWN, true],
+            [ZipConstants::ZIP64_MAGIC + 1, ZipConstants::ZIP64_MAGIC + 1, true],
+            [ZipConstants::ZIP64_MAGIC, ZipConstants::ZIP64_MAGIC, false],
+            [ZipConstants::ZIP64_MAGIC, ZipEntry::UNKNOWN, false],
+            [ZipEntry::UNKNOWN, ZipConstants::ZIP64_MAGIC, false],
+        ];
+    }
+
+    /**
+     * @dataProvider provideExtraTime
+     *
+     * @param ExtraFieldsCollection   $extraFieldsCollection
+     * @param \DateTimeInterface      $mtime
+     * @param \DateTimeInterface|null $atime
+     * @param \DateTimeInterface|null $ctime
+     */
+    public function testMTimeATimeCTime(ExtraFieldsCollection $extraFieldsCollection, $mtime, $atime, $ctime)
+    {
+        $unixTimestamp = time();
+
+        $zipEntry = new ZipEntry('entry');
+        $zipEntry->setTime($unixTimestamp);
+
+        // converting from unixtime to dos may occur with a small margin of error
+        static::assertLessThanOrEqual($zipEntry->getMTime()->getTimestamp() + 1, $unixTimestamp);
+        static::assertGreaterThanOrEqual($zipEntry->getMTime()->getTimestamp() - 1, $unixTimestamp);
+
+        static::assertNull($zipEntry->getATime());
+        static::assertNull($zipEntry->getCTime());
+
+        $zipEntry->getCdExtraFields()->addCollection($extraFieldsCollection);
+        $zipEntry->getLocalExtraFields()->addCollection($extraFieldsCollection);
+
+        static::assertNotNull($zipEntry->getMTime());
+
+        static::assertSame($zipEntry->getMTime()->getTimestamp(), $mtime->getTimestamp());
+
+        if ($atime !== null) {
+            static::assertSame($zipEntry->getATime()->getTimestamp(), $atime->getTimestamp());
+        } else {
+            static::assertNull($zipEntry->getATime());
+        }
+
+        if ($ctime !== null) {
+            static::assertSame($zipEntry->getCTime()->getTimestamp(), $ctime->getTimestamp());
+        } else {
+            static::assertNull($zipEntry->getCTime());
+        }
+    }
+
+    /**
+     * @throws \Exception
+     *
+     * @return array
+     */
+    public function provideExtraTime()
+    {
+        $ntfsExtra = NtfsExtraField::create(
+            new \DateTimeImmutable('-1 week'),
+            new \DateTimeImmutable('-1 month'),
+            new \DateTimeImmutable('-1 year')
+        );
+
+        $extendedTimestampExtraField = ExtendedTimestampExtraField::create(
+            strtotime('-2 weeks'),
+            strtotime('-2 months'),
+            strtotime('-2 years')
+        );
+
+        $oldUnixExtraField = new OldUnixExtraField(
+            strtotime('-3 weeks'),
+            strtotime('-3 months'),
+            1000,
+            1000
+        );
+
+        $ntfsTimeCollection = new ExtraFieldsCollection();
+        $ntfsTimeCollection->add($ntfsExtra);
+
+        $extendedTimestampCollection = new ExtraFieldsCollection();
+        $extendedTimestampCollection->add($extendedTimestampExtraField);
+
+        $oldUnixExtraFieldCollection = new ExtraFieldsCollection();
+        $oldUnixExtraFieldCollection->add($oldUnixExtraField);
+
+        $oldExtendedCollection = clone $oldUnixExtraFieldCollection;
+        $oldExtendedCollection->add($extendedTimestampExtraField);
+
+        $fullCollection = clone $oldExtendedCollection;
+        $fullCollection->add($ntfsExtra);
+
+        return [
+            [
+                $ntfsTimeCollection,
+                $ntfsExtra->getModifyDateTime(),
+                $ntfsExtra->getAccessDateTime(),
+                $ntfsExtra->getCreateDateTime(),
+            ],
+            [
+                $extendedTimestampCollection,
+                $extendedTimestampExtraField->getModifyDateTime(),
+                $extendedTimestampExtraField->getAccessDateTime(),
+                $extendedTimestampExtraField->getCreateDateTime(),
+            ],
+            [
+                $oldUnixExtraFieldCollection,
+                $oldUnixExtraField->getModifyDateTime(),
+                $oldUnixExtraField->getAccessDateTime(),
+                null,
+            ],
+            [
+                $oldExtendedCollection,
+                $extendedTimestampExtraField->getModifyDateTime(),
+                $extendedTimestampExtraField->getAccessDateTime(),
+                $extendedTimestampExtraField->getCreateDateTime(),
+            ],
+            [
+                $fullCollection,
+                $ntfsExtra->getModifyDateTime(),
+                $ntfsExtra->getAccessDateTime(),
+                $ntfsExtra->getCreateDateTime(),
+            ],
+        ];
+    }
+
+    /**
+     * @throws ZipException
+     */
+    public function testClone()
+    {
+        $newUnixExtra = new NewUnixExtraField();
+        $zipData = new ZipFileData(new \SplFileInfo(__FILE__));
+
+        $zipEntry = new ZipEntry('entry');
+        $zipEntry->addExtraField($newUnixExtra);
+        $zipEntry->setData($zipData);
+
+        $cloneEntry = clone $zipEntry;
+
+        static::assertNotSame($cloneEntry, $zipEntry);
+        static::assertNotSame($cloneEntry->getCdExtraFields(), $zipEntry->getCdExtraFields());
+        static::assertNotSame($cloneEntry->getLocalExtraFields(), $zipEntry->getLocalExtraFields());
+        static::assertNotSame($cloneEntry->getCdExtraField(NewUnixExtraField::HEADER_ID), $newUnixExtra);
+        static::assertNotSame($cloneEntry->getLocalExtraField(NewUnixExtraField::HEADER_ID), $newUnixExtra);
+        static::assertNotSame($cloneEntry->getData(), $zipData);
+    }
+
+    public function testExtraCollection()
+    {
+        $zipEntry = new ZipEntry('entry');
+        $cdCollection = $zipEntry->getCdExtraFields();
+        $localCollection = $zipEntry->getLocalExtraFields();
+
+        static::assertNotSame($cdCollection, $localCollection);
+
+        $anotherCollection = new ExtraFieldsCollection();
+        $anotherCollection->add(new JarMarkerExtraField());
+        $anotherCollection->add(new AsiExtraField(0100777, 1000, 1000));
+
+        $zipEntry->setCdExtraFields($anotherCollection);
+        static::assertSame($anotherCollection, $zipEntry->getCdExtraFields());
+        static::assertSame($localCollection, $zipEntry->getLocalExtraFields());
+
+        $zipEntry->setLocalExtraFields($anotherCollection);
+        static::assertSame($anotherCollection, $zipEntry->getLocalExtraFields());
+        static::assertSame($zipEntry->getCdExtraFields(), $zipEntry->getLocalExtraFields());
+
+        $newUnixExtraField = new NewUnixExtraField(1, 1000, 1000);
+        $zipEntry->getCdExtraFields()->add($newUnixExtraField);
+
+        static::assertSame($zipEntry->getCdExtraField(NewUnixExtraField::HEADER_ID), $newUnixExtraField);
+        static::assertSame($zipEntry->getLocalExtraField(NewUnixExtraField::HEADER_ID), $newUnixExtraField);
+    }
+}

+ 29 - 0
tests/ZipFileTest.php

@@ -2389,4 +2389,33 @@ class ZipFileTest extends ZipTestCase
         }
         $zipFile->close();
     }
+
+    /**
+     * @throws ZipException
+     */
+    public function testRenameWithRecompressData()
+    {
+        $entryName = 'file.txt';
+        $newEntryName = 'rename_file.txt';
+        $contents = str_repeat('Test' . \PHP_EOL, 1024);
+
+        $zipFile = new ZipFile();
+        $zipFile->addFromString($entryName, $contents, ZipCompressionMethod::DEFLATED);
+        $zipFile->saveAsFile($this->outputFilename);
+        $zipFile->close();
+
+        self::assertCorrectZipArchive($this->outputFilename);
+
+        $zipFile->openFile($this->outputFilename);
+        $zipFile->rename($entryName, $newEntryName);
+        $zipFile->setCompressionMethodEntry($newEntryName, ZipCompressionMethod::STORED);
+        $zipFile->saveAsFile($this->outputFilename);
+        $zipFile->close();
+
+        self::assertCorrectZipArchive($this->outputFilename);
+
+        $zipFile->openFile($this->outputFilename);
+        static::assertSame($zipFile->getEntry($newEntryName)->getCompressionMethod(), ZipCompressionMethod::STORED);
+        $zipFile->close();
+    }
 }

+ 1 - 2
tests/ZipPasswordTest.php

@@ -57,8 +57,7 @@ class ZipPasswordTest extends ZipFileSetTestCase
             try {
                 $zipFile[$entryName];
                 static::fail('Expected Exception has not been raised.');
-            } catch (ZipAuthenticationException $ae) {
-                static::assertContains('Invalid password', $ae->getMessage());
+            } catch (ZipException $e) {
             }
         }