Kaynağa Gözat

Merge branch 'release/3.3.0'

Ne-Lexa 6 yıl önce
ebeveyn
işleme
b3b676e3af
40 değiştirilmiş dosya ile 2619 ekleme ve 116 silme
  1. 3 3
      .travis.yml
  2. 1 0
      composer.json
  3. 6 0
      phpunit.xml
  4. 37 4
      src/Constants/ZipOptions.php
  5. 3 1
      src/IO/ZipWriter.php
  6. 45 3
      src/Model/Data/ZipNewData.php
  7. 16 0
      src/Model/ImmutableZipContainer.php
  8. 6 1
      src/Model/ZipContainer.php
  9. 8 24
      src/Util/FilesUtil.php
  10. 61 26
      src/ZipFile.php
  11. 9 4
      src/ZipFileInterface.php
  12. 126 0
      tests/CustomZipFormatTest.php
  13. 101 0
      tests/Extra/Fields/AbstractUnicodeExtraFieldTest.php
  14. 78 0
      tests/Extra/Fields/ApkAlignmentExtraFieldTest.php
  15. 101 0
      tests/Extra/Fields/AsiExtraFieldTest.php
  16. 158 0
      tests/Extra/Fields/ExtendedTimestampExtraFieldTest.php
  17. 55 0
      tests/Extra/Fields/JarMarkerExtraFieldTest.php
  18. 97 0
      tests/Extra/Fields/NewUnixExtraFieldTest.php
  19. 245 0
      tests/Extra/Fields/NtfsExtraFieldTest.php
  20. 156 0
      tests/Extra/Fields/OldUnixExtraFieldTest.php
  21. 42 0
      tests/Extra/Fields/UnicodeCommentExtraFieldTest.php
  22. 56 0
      tests/Extra/Fields/UnicodePathExtraFieldTest.php
  23. 57 0
      tests/Extra/Fields/UnrecognizedExtraFieldTest.php
  24. 240 0
      tests/Extra/Fields/WinZipAesExtraFieldTest.php
  25. 132 0
      tests/Extra/Fields/Zip64ExtraFieldTest.php
  26. 39 0
      tests/Internal/CustomZip/CustomZipWriter.php
  27. 20 0
      tests/Internal/CustomZip/ZipFileCustomWriter.php
  28. 28 0
      tests/Internal/CustomZip/ZipFileWithBeforeSave.php
  29. 98 0
      tests/Internal/Epub/EpubFile.php
  30. 159 0
      tests/Internal/Epub/EpubInfo.php
  31. 21 0
      tests/Internal/Epub/EpubReader.php
  32. 66 0
      tests/Internal/Epub/EpubWriter.php
  33. 22 0
      tests/Internal/Epub/EpubZipContainer.php
  34. 88 0
      tests/SymlinkTest.php
  35. 2 1
      tests/Zip64Test.php
  36. 49 0
      tests/ZipEntryTest.php
  37. 0 41
      tests/ZipEventTest.php
  38. 70 8
      tests/ZipFileTest.php
  39. 118 0
      tests/ZipInfoTest.php
  40. BIN
      tests/resources/Advanced-v1.0.0.epub

+ 3 - 3
.travis.yml

@@ -2,15 +2,15 @@ language: php
 
 env:
   global:
-    - ZIPALIGN_INSTALL=false
     - COVERAGE=false
-    - PHPUNIT_FLAGS="-v -c phpunit.xml --testsuite only_fast_tests"
+    - PHPUNIT_FLAGS="-v -c phpunit.xml"
 
 matrix:
   include:
     - php: 5.5
       os: linux
       dist: trusty
+      env: ZIPALIGN_INSTALL=false
 
     - php: 5.6
       os: linux
@@ -40,7 +40,7 @@ matrix:
     - php: 7.4
       os: linux
       dist: xenial
-      env: COVERAGE=true ZIPALIGN_INSTALL=true PHPUNIT_FLAGS="-v -c phpunit.xml --testsuite only_fast_tests --coverage-clover=coverage.clover"
+      env: COVERAGE=true ZIPALIGN_INSTALL=true PHPUNIT_FLAGS="-v -c phpunit.xml --coverage-clover=coverage.clover"
 
 before_install:
   - if [[ $COVERAGE != true ]]; then phpenv config-rm xdebug.ini || true; fi

+ 1 - 0
composer.json

@@ -31,6 +31,7 @@
         "ext-bz2": "*",
         "ext-openssl": "*",
         "ext-fileinfo": "*",
+        "ext-xml": "*",
         "guzzlehttp/psr7": "^1.6",
         "phpunit/phpunit": "^4.8|^5.7",
         "symfony/var-dumper": "^3.0|^4.0|^5.0"

+ 6 - 0
phpunit.xml

@@ -22,6 +22,12 @@
         </testsuite>
     </testsuites>
 
+    <groups>
+        <exclude>
+            <group>large</group>
+        </exclude>
+    </groups>
+
     <filter>
         <whitelist>
             <directory>src</directory>

+ 37 - 4
src/Constants/ZipOptions.php

@@ -2,6 +2,9 @@
 
 namespace PhpZip\Constants;
 
+use PhpZip\IO\ZipReader;
+use PhpZip\ZipFile;
+
 /**
  * Interface ZipOptions.
  */
@@ -10,20 +13,50 @@ interface ZipOptions
     /**
      * Boolean option for store just file names (skip directory names).
      *
-     * @var string
+     * @see ZipFile::addFromFinder()
      */
     const STORE_ONLY_FILES = 'only_files';
 
-    /** @var string */
+    /**
+     * Uses the specified compression method.
+     *
+     * @see ZipFile::addFromFinder()
+     * @see ZipFile::addSplFile()
+     */
     const COMPRESSION_METHOD = 'compression_method';
 
-    /** @var string */
+    /**
+     * Set the specified record modification time.
+     * The value can be {@see \DateTimeInterface}, integer timestamp
+     * or a string of any format.
+     *
+     * @see ZipFile::addFromFinder()
+     * @see ZipFile::addSplFile()
+     */
     const MODIFIED_TIME = 'mtime';
 
     /**
-     * @var string
+     * Specifies the encoding of the record name for cases when the UTF-8
+     * usage flag is not set.
      *
+     * The most commonly used encodings are compiled into the constants
+     * of the {@see DosCodePage} class.
+     *
+     * @see ZipFile::openFile()
+     * @see ZipFile::openFromString()
+     * @see ZipFile::openFromStream()
+     * @see ZipReader::getDefaultOptions()
      * @see DosCodePage::getCodePages()
      */
     const CHARSET = 'charset';
+
+    /**
+     * Allows ({@see true}) or denies ({@see false}) unpacking unix symlinks.
+     *
+     * This is a potentially dangerous operation for uncontrolled zip files.
+     * By default is ({@see false}).
+     *
+     * @see https://josipfranjkovic.blogspot.com/2014/12/reading-local-files-from-facebooks.html
+     */
+    const EXTRACT_SYMLINKS = 'extract_symlinks';
 }

+ 3 - 1
src/IO/ZipWriter.php

@@ -39,7 +39,9 @@ class ZipWriter
      */
     public function __construct(ZipContainer $container)
     {
-        $this->zipContainer = $container;
+        // we clone the container so that the changes made to
+        // it do not affect the data in the ZipFile class
+        $this->zipContainer = clone $container;
     }
 
     /**

+ 45 - 3
src/Model/Data/ZipNewData.php

@@ -4,18 +4,27 @@ namespace PhpZip\Model\Data;
 
 use PhpZip\Model\ZipData;
 use PhpZip\Model\ZipEntry;
+use PhpZip\ZipFile;
 
 /**
- * Class ZipNewData.
+ * The class contains a streaming resource with new content added to the ZIP archive.
  */
 class ZipNewData implements ZipData
 {
-    /** @var resource */
-    private $stream;
+    /**
+     * A static variable allows closing the stream in the destructor
+     * only if it is its sole holder.
+     *
+     * @var array<int, int> array of resource ids and the number of class clones
+     */
+    private static $guardClonedStream = [];
 
     /** @var ZipEntry */
     private $zipEntry;
 
+    /** @var resource */
+    private $stream;
+
     /**
      * ZipStringData constructor.
      *
@@ -38,6 +47,12 @@ class ZipNewData implements ZipData
         } elseif (\is_resource($data)) {
             $this->stream = $data;
         }
+
+        $resourceId = (int) $this->stream;
+        self::$guardClonedStream[$resourceId] =
+            isset(self::$guardClonedStream[$resourceId]) ?
+                self::$guardClonedStream[$resourceId] + 1 :
+                0;
     }
 
     /**
@@ -79,8 +94,35 @@ class ZipNewData implements ZipData
         stream_copy_to_stream($stream, $outStream);
     }
 
+    /**
+     * @see https://php.net/manual/en/language.oop5.cloning.php
+     */
+    public function __clone()
+    {
+        $resourceId = (int) $this->stream;
+        self::$guardClonedStream[$resourceId] =
+            isset(self::$guardClonedStream[$resourceId]) ?
+                self::$guardClonedStream[$resourceId] + 1 :
+                1;
+    }
+
+    /**
+     * The stream will be closed when closing the zip archive.
+     *
+     * The method implements protection against closing the stream of the cloned object.
+     *
+     * @see ZipFile::close()
+     */
     public function __destruct()
     {
+        $resourceId = (int) $this->stream;
+
+        if (isset(self::$guardClonedStream[$resourceId]) && self::$guardClonedStream[$resourceId] > 0) {
+            self::$guardClonedStream[$resourceId]--;
+
+            return;
+        }
+
         if (\is_resource($this->stream)) {
             fclose($this->stream);
         }

+ 16 - 0
src/Model/ImmutableZipContainer.php

@@ -53,4 +53,20 @@ class ImmutableZipContainer implements \Countable
     {
         return \count($this->entries);
     }
+
+    /**
+     * When an object is cloned, PHP 5 will perform a shallow copy of all of the object's properties.
+     * Any properties that are references to other variables, will remain references.
+     * Once the cloning is complete, if a __clone() method is defined,
+     * then the newly created object's __clone() method will be called, to allow any necessary properties that need to
+     * be changed. NOT CALLABLE DIRECTLY.
+     *
+     * @see https://php.net/manual/en/language.oop5.cloning.php
+     */
+    public function __clone()
+    {
+        foreach ($this->entries as $key => $value) {
+            $this->entries[$key] = clone $value;
+        }
+    }
 }

+ 6 - 1
src/Model/ZipContainer.php

@@ -12,7 +12,12 @@ use PhpZip\Exception\ZipException;
  */
 class ZipContainer extends ImmutableZipContainer
 {
-    /** @var ImmutableZipContainer|null */
+    /**
+     * @var ImmutableZipContainer|null The source container contains zip entries from
+     *                                 an open zip archive. The source container makes
+     *                                 it possible to undo changes in the archive.
+     *                                 When cloning, this container is not cloned.
+     */
     private $sourceContainer;
 
     /**

+ 8 - 24
src/Util/FilesUtil.php

@@ -43,9 +43,10 @@ final class FilesUtil
             \RecursiveIteratorIterator::CHILD_FIRST
         );
 
+        /** @var \SplFileInfo $fileInfo */
         foreach ($files as $fileInfo) {
             $function = ($fileInfo->isDir() ? 'rmdir' : 'unlink');
-            $function($fileInfo->getRealPath());
+            $function($fileInfo->getPathname());
         }
         rmdir($dir);
     }
@@ -303,36 +304,19 @@ final class FilesUtil
     }
 
     /**
-     * @param string $linkPath
      * @param string $target
+     * @param string $path
+     * @param bool   $allowSymlink
      *
      * @return bool
      */
-    public static function symlink($target, $linkPath)
+    public static function symlink($target, $path, $allowSymlink)
     {
-        if (\DIRECTORY_SEPARATOR === '\\') {
-            $linkPath = str_replace('/', '\\', $linkPath);
-            $target = str_replace('/', '\\', $target);
-            $abs = null;
-
-            if (!self::isAbsolutePath($target)) {
-                $abs = realpath(\dirname($linkPath) . \DIRECTORY_SEPARATOR . $target);
-
-                if (\is_string($abs)) {
-                    $target = $abs;
-                }
-            }
-        }
-
-        if (!symlink($target, $linkPath)) {
-            if (\DIRECTORY_SEPARATOR === '\\' && is_file($target)) {
-                return copy($target, $linkPath);
-            }
-
-            return false;
+        if (\DIRECTORY_SEPARATOR === '\\' || !$allowSymlink) {
+            return file_put_contents($path, $target) !== false;
         }
 
-        return true;
+        return symlink($target, $path);
     }
 
     /**

+ 61 - 26
src/ZipFile.php

@@ -6,6 +6,7 @@
 
 namespace PhpZip;
 
+use PhpZip\Constants\UnixStat;
 use PhpZip\Constants\ZipCompressionLevel;
 use PhpZip\Constants\ZipCompressionMethod;
 use PhpZip\Constants\ZipEncryptionMethod;
@@ -20,6 +21,7 @@ use PhpZip\IO\ZipReader;
 use PhpZip\IO\ZipWriter;
 use PhpZip\Model\Data\ZipFileData;
 use PhpZip\Model\Data\ZipNewData;
+use PhpZip\Model\ImmutableZipContainer;
 use PhpZip\Model\ZipContainer;
 use PhpZip\Model\ZipEntry;
 use PhpZip\Model\ZipEntryMatcher;
@@ -68,7 +70,7 @@ class ZipFile implements ZipFileInterface
      */
     public function __construct()
     {
-        $this->zipContainer = new ZipContainer();
+        $this->zipContainer = $this->createZipContainer(null);
     }
 
     /**
@@ -90,6 +92,16 @@ class ZipFile implements ZipFileInterface
         return new ZipWriter($this->zipContainer);
     }
 
+    /**
+     * @param ImmutableZipContainer|null $sourceContainer
+     *
+     * @return ZipContainer
+     */
+    protected function createZipContainer(ImmutableZipContainer $sourceContainer = null)
+    {
+        return new ZipContainer($sourceContainer);
+    }
+
     /**
      * Open zip archive from file.
      *
@@ -130,7 +142,9 @@ class ZipFile implements ZipFileInterface
         }
 
         if (!($handle = fopen('php://temp', 'r+b'))) {
+            // @codeCoverageIgnoreStart
             throw new ZipException("Can't open temp stream.");
+            // @codeCoverageIgnoreEnd
         }
         fwrite($handle, $data);
         rewind($handle);
@@ -151,7 +165,7 @@ class ZipFile implements ZipFileInterface
     public function openFromStream($handle, array $options = [])
     {
         $this->reader = $this->createZipReader($handle, $options);
-        $this->zipContainer = new ZipContainer($this->reader->read());
+        $this->zipContainer = $this->createZipContainer($this->reader->read());
 
         return $this;
     }
@@ -363,15 +377,20 @@ class ZipFile implements ZipFileInterface
      *
      * Extract the complete archive or the given files to the specified destination.
      *
-     * @param string            $destDir location where to extract the files
-     * @param array|string|null $entries The entries to extract. It accepts either
-     *                                   a single entry name or an array of names.
+     * @param string            $destDir          location where to extract the files
+     * @param array|string|null $entries          entries to extract
+     * @param array             $options          extract options
+     * @param array             $extractedEntries if the extractedEntries argument
+     *                                            is present, then the  specified
+     *                                            array will be filled with
+     *                                            information about the
+     *                                            extracted entries
      *
      * @throws ZipException
      *
      * @return ZipFile
      */
-    public function extractTo($destDir, $entries = null)
+    public function extractTo($destDir, $entries = null, array $options = [], &$extractedEntries = [])
     {
         if (!file_exists($destDir)) {
             throw new ZipException(sprintf('Destination %s not found', $destDir));
@@ -385,7 +404,14 @@ class ZipFile implements ZipFileInterface
             throw new ZipException('Destination is not writable directory');
         }
 
-        $extractedEntries = [];
+        if ($extractedEntries === null) {
+            $extractedEntries = [];
+        }
+
+        $defaultOptions = [
+            ZipOptions::EXTRACT_SYMLINKS => false,
+        ];
+        $options += $defaultOptions;
 
         $zipEntries = $this->zipContainer->getEntries();
 
@@ -435,7 +461,9 @@ class ZipFile implements ZipFileInterface
                 }
 
                 if (!mkdir($dir, $dirMode, true) && !is_dir($dir)) {
+                    // @codeCoverageIgnoreStart
                     throw new \RuntimeException(sprintf('Directory "%s" was not created', $dir));
+                    // @codeCoverageIgnoreEnd
                 }
                 chmod($dir, $dirMode);
             }
@@ -471,6 +499,7 @@ class ZipFile implements ZipFileInterface
 
             /** @noinspection PhpUsageOfSilenceOperatorInspection */
             if (!($handle = @fopen($file, 'w+b'))) {
+                // @codeCoverageIgnoreStart
                 throw new ZipException(
                     sprintf(
                         'Cannot extract zip entry %s. File %s cannot open for write.',
@@ -478,6 +507,7 @@ class ZipFile implements ZipFileInterface
                         $file
                     )
                 );
+                // @codeCoverageIgnoreEnd
             }
 
             try {
@@ -486,9 +516,8 @@ class ZipFile implements ZipFileInterface
                 unlink($file);
 
                 throw $e;
-            } finally {
-                fclose($handle);
             }
+            fclose($handle);
 
             if ($unixMode === 0) {
                 $unixMode = 0644;
@@ -503,8 +532,10 @@ class ZipFile implements ZipFileInterface
             }
         }
 
+        $allowSymlink = (bool) $options[ZipOptions::EXTRACT_SYMLINKS];
+
         foreach ($symlinks as $linkPath => $target) {
-            if (!FilesUtil::symlink($target, $linkPath)) {
+            if (!FilesUtil::symlink($target, $linkPath, $allowSymlink)) {
                 unset($extractedEntries[$linkPath]);
             }
         }
@@ -515,7 +546,7 @@ class ZipFile implements ZipFileInterface
             touch($dir, $lastMod);
         }
 
-//        ksort($extractedEntries);
+        ksort($extractedEntries);
 
         return $this;
     }
@@ -652,9 +683,24 @@ class ZipFile implements ZipFileInterface
         $entryName = $file->isDir() ? rtrim($entryName, '/\\') . '/' : $entryName;
 
         $zipEntry = new ZipEntry($entryName);
+        $zipEntry->setCreatedOS(ZipPlatform::OS_UNIX);
+        $zipEntry->setExtractedOS(ZipPlatform::OS_UNIX);
+
         $zipData = null;
+        $filePerms = $file->getPerms();
+
+        if ($file->isLink()) {
+            $linkTarget = $file->getLinkTarget();
+            $lengthLinkTarget = \strlen($linkTarget);
 
-        if ($file->isFile()) {
+            $zipEntry->setCompressionMethod(ZipCompressionMethod::STORED);
+            $zipEntry->setUncompressedSize($lengthLinkTarget);
+            $zipEntry->setCompressedSize($lengthLinkTarget);
+            $zipEntry->setCrc(crc32($linkTarget));
+            $filePerms |= UnixStat::UNX_IFLNK;
+
+            $zipData = new ZipNewData($zipEntry, $linkTarget);
+        } elseif ($file->isFile()) {
             if (isset($options[ZipOptions::COMPRESSION_METHOD])) {
                 $compressionMethod = $options[ZipOptions::COMPRESSION_METHOD];
             } elseif ($file->getSize() < 512) {
@@ -673,21 +719,9 @@ class ZipFile implements ZipFileInterface
             $zipEntry->setUncompressedSize(0);
             $zipEntry->setCompressedSize(0);
             $zipEntry->setCrc(0);
-        } elseif ($file->isLink()) {
-            $linkTarget = $file->getLinkTarget();
-            $lengthLinkTarget = \strlen($linkTarget);
-
-            $zipEntry->setCompressionMethod(ZipCompressionMethod::STORED);
-            $zipEntry->setUncompressedSize($lengthLinkTarget);
-            $zipEntry->setCompressedSize($lengthLinkTarget);
-            $zipEntry->setCrc(crc32($linkTarget));
-
-            $zipData = new ZipNewData($zipEntry, $linkTarget);
         }
 
-        $zipEntry->setCreatedOS(ZipPlatform::OS_UNIX);
-        $zipEntry->setExtractedOS(ZipPlatform::OS_UNIX);
-        $zipEntry->setUnixMode($file->getPerms());
+        $zipEntry->setUnixMode($filePerms);
 
         $timestamp = null;
 
@@ -1768,8 +1802,9 @@ class ZipFile implements ZipFileInterface
         if ($this->reader !== null) {
             $this->reader->close();
             $this->reader = null;
-            $this->zipContainer = new ZipContainer();
         }
+        $this->zipContainer = $this->createZipContainer(null);
+        gc_collect_cycles();
     }
 
     /**

+ 9 - 4
src/ZipFileInterface.php

@@ -292,15 +292,20 @@ interface ZipFileInterface extends \Countable, \ArrayAccess, \Iterator
      *
      * Extract the complete archive or the given files to the specified destination.
      *
-     * @param string            $destDir location where to extract the files
-     * @param array|string|null $entries The entries to extract. It accepts either
-     *                                   a single entry name or an array of names.
+     * @param string            $destDir          location where to extract the files
+     * @param array|string|null $entries          entries to extract
+     * @param array             $options          extract options
+     * @param array             $extractedEntries if the extractedEntries argument
+     *                                            is present, then the  specified
+     *                                            array will be filled with
+     *                                            information about the
+     *                                            extracted entries
      *
      * @throws ZipException
      *
      * @return ZipFile
      */
-    public function extractTo($destDir, $entries = null);
+    public function extractTo($destDir, $entries = null, array $options = [], &$extractedEntries = []);
 
     /**
      * Add entry from the string.

+ 126 - 0
tests/CustomZipFormatTest.php

@@ -0,0 +1,126 @@
+<?php
+
+namespace PhpZip\Tests;
+
+use PhpZip\Exception\ZipEntryNotFoundException;
+use PhpZip\Exception\ZipException;
+use PhpZip\Model\Extra\Fields\NewUnixExtraField;
+use PhpZip\Model\Extra\Fields\NtfsExtraField;
+use PhpZip\Tests\Internal\CustomZip\ZipFileCustomWriter;
+use PhpZip\Tests\Internal\CustomZip\ZipFileWithBeforeSave;
+use PhpZip\Tests\Internal\Epub\EpubFile;
+use PhpZip\ZipFile;
+
+/**
+ * Checks the ability to create own file-type class, reader, writer and container.
+ **.
+ *
+ * @internal
+ *
+ * @small
+ */
+final class CustomZipFormatTest extends ZipTestCase
+{
+    /**
+     * @throws ZipException
+     *
+     * @see http://www.epubtest.org/test-books source epub files
+     */
+    public function testEpub()
+    {
+        $epubFile = new EpubFile();
+        $epubFile->openFile(__DIR__ . '/resources/Advanced-v1.0.0.epub');
+        self::assertSame($epubFile->getRootFile(), 'EPUB/package.opf');
+        self::assertSame($epubFile->getMimeType(), 'application/epub+zip');
+        $epubInfo = $epubFile->getEpubInfo();
+        self::assertSame($epubInfo->toArray(), [
+            'title' => 'Advanced Accessibility Tests: Extended Descriptions',
+            'creator' => 'DAISY Consortium Transition to EPUB 3 and DIAGRAM Standards WG',
+            'language' => 'en-US',
+            'publisher' => 'DAISY Consortium and DIAGRAM Center',
+            'description' => 'Tests for accessible extended descriptions of images in EPUBs',
+            'rights' => 'This work is licensed under a Creative Commons Attribution-Noncommercial-Share Alike (CC BY-NC-SA) license.',
+            'date' => '2019-01-03',
+            'subject' => 'extended-descriptions',
+        ]);
+        $epubFile->deleteFromName('mimetype');
+        self::assertFalse($epubFile->hasEntry('mimetype'));
+
+        try {
+            $epubFile->getMimeType();
+            self::fail('deleted mimetype');
+        } catch (ZipEntryNotFoundException $e) {
+            self::assertSame('Zip Entry "mimetype" was not found in the archive.', $e->getMessage());
+        }
+        $epubFile->saveAsFile($this->outputFilename);
+        self::assertFalse($epubFile->hasEntry('mimetype'));
+        $epubFile->close();
+
+        self::assertCorrectZipArchive($this->outputFilename);
+
+        $epubFile->openFile($this->outputFilename);
+        // file appended in EpubWriter before write
+        self::assertTrue($epubFile->hasEntry('mimetype'));
+        $epubFile->close();
+    }
+
+    /**
+     * @throws \Exception
+     */
+    public function testBeforeSaveInZipWriter()
+    {
+        $zipFile = new ZipFileCustomWriter();
+        for ($i = 0; $i < 10; $i++) {
+            $zipFile['file ' . $i] = 'contents file ' . $i;
+        }
+        $this->existsExtraFields($zipFile, false);
+        $zipFile->saveAsFile($this->outputFilename);
+        $this->existsExtraFields($zipFile, false);
+        $zipFile->close();
+
+        self::assertCorrectZipArchive($this->outputFilename);
+
+        $zipFile->openFile($this->outputFilename);
+        $this->existsExtraFields($zipFile, true);
+        $zipFile->close();
+    }
+
+    /**
+     * @throws \Exception
+     */
+    public function testBeforeSaveInZipFile()
+    {
+        $zipFile = new ZipFileWithBeforeSave();
+        for ($i = 0; $i < 10; $i++) {
+            $zipFile['file ' . $i] = 'contents file ' . $i;
+        }
+        $this->existsExtraFields($zipFile, false);
+        $zipFile->saveAsFile($this->outputFilename);
+        $this->existsExtraFields($zipFile, true);
+        $zipFile->close();
+
+        self::assertCorrectZipArchive($this->outputFilename);
+
+        $zipFile->openFile($this->outputFilename);
+        $this->existsExtraFields($zipFile, true);
+        $zipFile->close();
+    }
+
+    /**
+     * @param ZipFile $zipFile
+     * @param bool    $exists
+     */
+    private function existsExtraFields(ZipFile $zipFile, $exists)
+    {
+        foreach ($zipFile->getEntries() as $entry) {
+            $localExtras = $entry->getLocalExtraFields();
+            $cdExtras = $entry->getCdExtraFields();
+
+            self::assertSame(isset($localExtras[NtfsExtraField::HEADER_ID]), $exists);
+            self::assertSame(isset($cdExtras[NtfsExtraField::HEADER_ID]), $exists);
+
+            self::assertSame(isset($localExtras[NewUnixExtraField::HEADER_ID]), $exists);
+            self::assertSame(isset($cdExtras[NewUnixExtraField::HEADER_ID]), $exists);
+        }
+    }
+}

+ 101 - 0
tests/Extra/Fields/AbstractUnicodeExtraFieldTest.php

@@ -0,0 +1,101 @@
+<?php
+
+/** @noinspection PhpUndefinedMethodInspection */
+
+namespace PhpZip\Tests\Extra\Fields;
+
+use PHPUnit\Framework\TestCase;
+use PhpZip\Exception\ZipException;
+use PhpZip\Model\Extra\Fields\AbstractUnicodeExtraField;
+
+/**
+ * Class AbstractUnicodeExtraFieldTest.
+ */
+abstract class AbstractUnicodeExtraFieldTest extends TestCase
+{
+    /**
+     * @return string|AbstractUnicodeExtraField
+     *
+     * @psalm-var class-string<\PhpZip\Model\Extra\Fields\AbstractUnicodeExtraField>
+     */
+    abstract protected function getUnicodeExtraFieldClassName();
+
+    /**
+     * @dataProvider provideExtraField
+     *
+     * @param int    $crc32
+     * @param string $unicodePath
+     * @param string $originalPath
+     * @param string $binaryData
+     *
+     * @throws ZipException
+     */
+    public function testExtraField($crc32, $unicodePath, $originalPath, $binaryData)
+    {
+        $crc32 = (int) $crc32; // for php 32-bit
+
+        $className = $this->getUnicodeExtraFieldClassName();
+
+        /** @var AbstractUnicodeExtraField $extraField */
+        $extraField = new $className($crc32, $unicodePath);
+        static::assertSame($extraField->getCrc32(), $crc32);
+        static::assertSame($extraField->getUnicodeValue(), $unicodePath);
+        static::assertSame(crc32($originalPath), $crc32);
+
+        static::assertSame($binaryData, $extraField->packLocalFileData());
+        static::assertSame($binaryData, $extraField->packCentralDirData());
+        static::assertEquals($className::unpackLocalFileData($binaryData), $extraField);
+        static::assertEquals($className::unpackCentralDirData($binaryData), $extraField);
+    }
+
+    /**
+     * @return array
+     */
+    abstract public function provideExtraField();
+
+    public function testSetter()
+    {
+        $className = $this->getUnicodeExtraFieldClassName();
+        $entryName = '11111';
+
+        /** @var AbstractUnicodeExtraField $extraField */
+        $extraField = new $className(crc32($entryName), '22222');
+        static::assertSame($extraField->getHeaderId(), $className::HEADER_ID);
+        static::assertSame($extraField->getCrc32(), crc32($entryName));
+        static::assertSame($extraField->getUnicodeValue(), '22222');
+
+        $crc32 = 1234567;
+        $extraField->setCrc32($crc32);
+        static::assertSame($extraField->getCrc32(), $crc32);
+        $extraField->setUnicodeValue('44444');
+        static::assertSame($extraField->getUnicodeValue(), '44444');
+    }
+
+    /**
+     * @throws ZipException
+     */
+    public function testUnicodeErrorParse()
+    {
+        $this->setExpectedException(
+            ZipException::class,
+            'Unicode path extra data must have at least 5 bytes.'
+        );
+
+        $className = $this->getUnicodeExtraFieldClassName();
+        $className::unpackLocalFileData('');
+    }
+
+    /**
+     * @throws ZipException
+     */
+    public function testUnknownVersionParse()
+    {
+        $this->setExpectedException(
+            ZipException::class,
+            'Unsupported version [2] for Unicode path extra data.'
+        );
+
+        $className = $this->getUnicodeExtraFieldClassName();
+        $className::unpackLocalFileData("\x02\x04a\xD28\xC3\xA4\\\xC3\xBC.txt");
+    }
+}

Dosya farkı çok büyük olduğundan ihmal edildi
+ 78 - 0
tests/Extra/Fields/ApkAlignmentExtraFieldTest.php


+ 101 - 0
tests/Extra/Fields/AsiExtraFieldTest.php

@@ -0,0 +1,101 @@
+<?php
+
+namespace PhpZip\Tests\Extra\Fields;
+
+use PHPUnit\Framework\TestCase;
+use PhpZip\Exception\Crc32Exception;
+use PhpZip\Exception\ZipException;
+use PhpZip\Model\Extra\Fields\AsiExtraField;
+
+/**
+ * @internal
+ *
+ * @small
+ */
+final class AsiExtraFieldTest extends TestCase
+{
+    /**
+     * @dataProvider provideExtraField
+     *
+     * @param int    $mode
+     * @param int    $uid
+     * @param int    $gid
+     * @param string $link
+     * @param string $binaryData
+     *
+     * @throws ZipException
+     */
+    public function testExtraField($mode, $uid, $gid, $link, $binaryData)
+    {
+        $asiExtraField = new AsiExtraField($mode, $uid, $gid, $link);
+        self::assertSame($asiExtraField->getHeaderId(), AsiExtraField::HEADER_ID);
+
+        self::assertSame($asiExtraField->getMode(), $mode);
+        self::assertSame($asiExtraField->getUserId(), $uid);
+        self::assertSame($asiExtraField->getGroupId(), $uid);
+        self::assertSame($asiExtraField->getLink(), $link);
+
+        self::assertSame($asiExtraField->packLocalFileData(), $binaryData);
+        self::assertSame($asiExtraField->packCentralDirData(), $binaryData);
+
+        self::assertEquals(AsiExtraField::unpackLocalFileData($binaryData), $asiExtraField);
+        self::assertEquals(AsiExtraField::unpackCentralDirData($binaryData), $asiExtraField);
+    }
+
+    /**
+     * @return array
+     */
+    public function provideExtraField()
+    {
+        return [
+            [
+                040755,
+                AsiExtraField::USER_GID_PID,
+                AsiExtraField::USER_GID_PID,
+                '',
+                "#\x06\\\xF6\xEDA\x00\x00\x00\x00\xE8\x03\xE8\x03",
+            ],
+            [
+                0100644,
+                0,
+                0,
+                'sites-enabled/example.conf',
+                "_\xB8\xC7b\xA4\x81\x1A\x00\x00\x00\x00\x00\x00\x00sites-enabled/example.conf",
+            ],
+        ];
+    }
+
+    public function testSetter()
+    {
+        $extraField = new AsiExtraField(0777);
+        $extraField->setMode(0100666);
+        self::assertSame(0100666, $extraField->getMode());
+        $extraField->setUserId(700);
+        self::assertSame(700, $extraField->getUserId());
+        $extraField->setGroupId(500);
+        self::assertSame(500, $extraField->getGroupId());
+        $extraField->setLink('link.txt');
+        self::assertSame($extraField->getLink(), 'link.txt');
+        self::assertSame(0120666, $extraField->getMode());
+
+        // dir mode
+        $extraField->setMode(0755);
+        self::assertSame(0120755, $extraField->getMode());
+        $extraField->setLink('');
+        self::assertSame($extraField->getLink(), '');
+        self::assertSame(0100755, $extraField->getMode());
+    }
+
+    /**
+     * @throws Crc32Exception
+     */
+    public function testInvalidParse()
+    {
+        $this->setExpectedException(
+            Crc32Exception::class,
+            'Asi Unix Extra Filed Data (expected CRC32 value'
+        );
+
+        AsiExtraField::unpackLocalFileData("\x01\x06\\\xF6\xEDA\x00\x00\x00\x00\xE8\x03\xE8\x03");
+    }
+}

+ 158 - 0
tests/Extra/Fields/ExtendedTimestampExtraFieldTest.php

@@ -0,0 +1,158 @@
+<?php
+
+namespace PhpZip\Tests\Extra\Fields;
+
+use PHPUnit\Framework\TestCase;
+use PhpZip\Model\Extra\Fields\ExtendedTimestampExtraField;
+
+/**
+ * Class ExtendedTimestampExtraFieldTest.
+ *
+ * @internal
+ *
+ * @small
+ */
+final class ExtendedTimestampExtraFieldTest extends TestCase
+{
+    /**
+     * @dataProvider provideExtraField
+     *
+     * @param int      $flags
+     * @param int|null $modifyTime
+     * @param int|null $accessTime
+     * @param int|null $createTime
+     * @param string   $localData
+     * @param string   $cdData
+     *
+     * @noinspection PhpTooManyParametersInspection
+     */
+    public function testExtraField(
+        $flags,
+        $modifyTime,
+        $accessTime,
+        $createTime,
+        $localData,
+        $cdData
+    ) {
+        $localExtraField = new ExtendedTimestampExtraField($flags, $modifyTime, $accessTime, $createTime);
+        self::assertSame($localExtraField->getHeaderId(), ExtendedTimestampExtraField::HEADER_ID);
+        self::assertSame($localExtraField->getFlags(), $flags);
+        self::assertSame($localExtraField->getModifyTime(), $modifyTime);
+        self::assertSame($localExtraField->getAccessTime(), $accessTime);
+        self::assertSame($localExtraField->getCreateTime(), $createTime);
+        self::assertSame($localExtraField->packLocalFileData(), $localData);
+        self::assertEquals(ExtendedTimestampExtraField::unpackLocalFileData($localData), $localExtraField);
+
+        $extTimeField = ExtendedTimestampExtraField::create($modifyTime, $accessTime, $createTime);
+        self::assertEquals($extTimeField, $localExtraField);
+
+        $accessTime = null;
+        $createTime = null;
+        $cdExtraField = new ExtendedTimestampExtraField($flags, $modifyTime, $accessTime, $createTime);
+        self::assertSame($cdExtraField->getHeaderId(), ExtendedTimestampExtraField::HEADER_ID);
+        self::assertSame($cdExtraField->getFlags(), $flags);
+        self::assertSame($cdExtraField->getModifyTime(), $modifyTime);
+        self::assertSame($cdExtraField->getAccessTime(), $accessTime);
+        self::assertSame($cdExtraField->getCreateTime(), $createTime);
+        self::assertSame($cdExtraField->packCentralDirData(), $cdData);
+        self::assertEquals(ExtendedTimestampExtraField::unpackCentralDirData($cdData), $cdExtraField);
+        self::assertSame($localExtraField->packCentralDirData(), $cdData);
+    }
+
+    /**
+     * @return array
+     */
+    public function provideExtraField()
+    {
+        return [
+            [
+                ExtendedTimestampExtraField::MODIFY_TIME_BIT |
+                ExtendedTimestampExtraField::ACCESS_TIME_BIT |
+                ExtendedTimestampExtraField::CREATE_TIME_BIT,
+                911512006,
+                911430000,
+                893709400,
+                "\x07\xC6\x91T6pQS6X\xECD5",
+                "\x07\xC6\x91T6",
+            ],
+            [
+                ExtendedTimestampExtraField::MODIFY_TIME_BIT |
+                ExtendedTimestampExtraField::ACCESS_TIME_BIT,
+                1492955702,
+                1492955638,
+                null,
+                "\x036\xB2\xFCX\xF6\xB1\xFCX",
+                "\x036\xB2\xFCX",
+            ],
+            [
+                ExtendedTimestampExtraField::MODIFY_TIME_BIT,
+                1470494391,
+                null,
+                null,
+                "\x01\xB7\xF6\xA5W",
+                "\x01\xB7\xF6\xA5W",
+            ],
+        ];
+    }
+
+    /**
+     * @throws \Exception
+     */
+    public function testSetter()
+    {
+        $mtime = time();
+        $atime = null;
+        $ctime = null;
+
+        $field = ExtendedTimestampExtraField::create($mtime, $atime, $ctime);
+        self::assertSame($field->getFlags(), ExtendedTimestampExtraField::MODIFY_TIME_BIT);
+        self::assertSame($field->getModifyTime(), $mtime);
+        self::assertEquals($field->getModifyDateTime(), new \DateTimeImmutable('@' . $mtime));
+        self::assertSame($field->getAccessTime(), $atime);
+        self::assertSame($field->getCreateTime(), $ctime);
+
+        $atime = strtotime('-1 min');
+        $field->setAccessTime($atime);
+        self::assertSame(
+            $field->getFlags(),
+            ExtendedTimestampExtraField::MODIFY_TIME_BIT |
+            ExtendedTimestampExtraField::ACCESS_TIME_BIT
+        );
+        self::assertSame($field->getModifyTime(), $mtime);
+        self::assertSame($field->getAccessTime(), $atime);
+        self::assertEquals($field->getAccessDateTime(), new \DateTimeImmutable('@' . $atime));
+        self::assertSame($field->getCreateTime(), $ctime);
+
+        $ctime = strtotime('-1 hour');
+        $field->setCreateTime($ctime);
+        self::assertSame(
+            $field->getFlags(),
+            ExtendedTimestampExtraField::MODIFY_TIME_BIT |
+            ExtendedTimestampExtraField::ACCESS_TIME_BIT |
+            ExtendedTimestampExtraField::CREATE_TIME_BIT
+        );
+        self::assertSame($field->getModifyTime(), $mtime);
+        self::assertSame($field->getAccessTime(), $atime);
+        self::assertSame($field->getCreateTime(), $ctime);
+        self::assertEquals($field->getCreateDateTime(), new \DateTimeImmutable('@' . $ctime));
+
+        $field->setCreateTime(null);
+        self::assertNull($field->getCreateTime());
+        self::assertNull($field->getCreateDateTime());
+        self::assertSame(
+            $field->getFlags(),
+            ExtendedTimestampExtraField::MODIFY_TIME_BIT |
+            ExtendedTimestampExtraField::ACCESS_TIME_BIT
+        );
+
+        $field->setAccessTime(null);
+        self::assertNull($field->getAccessTime());
+        self::assertNull($field->getAccessDateTime());
+        self::assertSame($field->getFlags(), ExtendedTimestampExtraField::MODIFY_TIME_BIT);
+
+        $field->setModifyTime(null);
+        self::assertNull($field->getModifyTime());
+        self::assertNull($field->getModifyDateTime());
+        self::assertSame($field->getFlags(), 0);
+    }
+}

+ 55 - 0
tests/Extra/Fields/JarMarkerExtraFieldTest.php

@@ -0,0 +1,55 @@
+<?php
+
+namespace PhpZip\Tests\Extra\Fields;
+
+use PHPUnit\Framework\TestCase;
+use PhpZip\Exception\ZipException;
+use PhpZip\Model\Extra\Fields\JarMarkerExtraField;
+
+/**
+ * Class JarMarkerExtraFieldTest.
+ *
+ * @internal
+ *
+ * @small
+ */
+final class JarMarkerExtraFieldTest extends TestCase
+{
+    /**
+     * @throws ZipException
+     */
+    public function testExtraField()
+    {
+        $jarField = new JarMarkerExtraField();
+        self::assertSame('', $jarField->packLocalFileData());
+        self::assertSame('', $jarField->packCentralDirData());
+        self::assertEquals(JarMarkerExtraField::unpackLocalFileData(''), $jarField);
+        self::assertEquals(JarMarkerExtraField::unpackCentralDirData(''), $jarField);
+    }
+
+    /**
+     * @throws ZipException
+     */
+    public function testInvalidUnpackLocalData()
+    {
+        $this->setExpectedException(
+            ZipException::class,
+            "JarMarker doesn't expect any data"
+        );
+
+        JarMarkerExtraField::unpackLocalFileData("\x02\x00\00");
+    }
+
+    /**
+     * @throws ZipException
+     */
+    public function testInvalidUnpackCdData()
+    {
+        $this->setExpectedException(
+            ZipException::class,
+            "JarMarker doesn't expect any data"
+        );
+
+        JarMarkerExtraField::unpackCentralDirData("\x02\x00\00");
+    }
+}

+ 97 - 0
tests/Extra/Fields/NewUnixExtraFieldTest.php

@@ -0,0 +1,97 @@
+<?php
+
+namespace PhpZip\Tests\Extra\Fields;
+
+use PHPUnit\Framework\TestCase;
+use PhpZip\Exception\ZipException;
+use PhpZip\Model\Extra\Fields\NewUnixExtraField;
+
+/**
+ * Class NewUnixExtraFieldTest.
+ *
+ * @internal
+ *
+ * @small
+ */
+final class NewUnixExtraFieldTest extends TestCase
+{
+    /**
+     * @dataProvider provideExtraField
+     *
+     * @param int    $version
+     * @param int    $uid
+     * @param int    $gid
+     * @param string $binaryData
+     *
+     * @throws ZipException
+     */
+    public function testExtraField($version, $uid, $gid, $binaryData)
+    {
+        $extraField = new NewUnixExtraField($version, $uid, $gid);
+        self::assertSame($extraField->getHeaderId(), NewUnixExtraField::HEADER_ID);
+        self::assertSame($extraField->getVersion(), $version);
+        self::assertSame($extraField->getGid(), $gid);
+        self::assertSame($extraField->getUid(), $uid);
+
+        self::assertEquals(NewUnixExtraField::unpackLocalFileData($binaryData), $extraField);
+        self::assertEquals(NewUnixExtraField::unpackCentralDirData($binaryData), $extraField);
+
+        self::assertSame($extraField->packLocalFileData(), $binaryData);
+        self::assertSame($extraField->packCentralDirData(), $binaryData);
+    }
+
+    /**
+     * @return array
+     */
+    public function provideExtraField()
+    {
+        return [
+            [
+                1,
+                NewUnixExtraField::USER_GID_PID,
+                NewUnixExtraField::USER_GID_PID,
+                "\x01\x04\xE8\x03\x00\x00\x04\xE8\x03\x00\x00",
+            ],
+            [
+                1,
+                501,
+                20,
+                "\x01\x04\xF5\x01\x00\x00\x04\x14\x00\x00\x00",
+            ],
+            [
+                1,
+                500,
+                495,
+                "\x01\x04\xF4\x01\x00\x00\x04\xEF\x01\x00\x00",
+            ],
+            [
+                1,
+                11252,
+                10545,
+                "\x01\x04\xF4+\x00\x00\x041)\x00\x00",
+            ],
+            [
+                1,
+                1721,
+                1721,
+                "\x01\x04\xB9\x06\x00\x00\x04\xB9\x06\x00\x00",
+            ],
+        ];
+    }
+
+    public function testSetter()
+    {
+        $extraField = new NewUnixExtraField(1, 1000, 1000);
+        self::assertSame(1, $extraField->getVersion());
+        self::assertSame(1000, $extraField->getUid());
+        self::assertSame(1000, $extraField->getGid());
+
+        $extraField->setUid(0);
+        self::assertSame(0, $extraField->getUid());
+        self::assertSame(1000, $extraField->getGid());
+
+        $extraField->setGid(0);
+        self::assertSame(0, $extraField->getUid());
+        self::assertSame(0, $extraField->getGid());
+    }
+}

+ 245 - 0
tests/Extra/Fields/NtfsExtraFieldTest.php

@@ -0,0 +1,245 @@
+<?php
+
+namespace PhpZip\Tests\Extra\Fields;
+
+use PHPUnit\Framework\TestCase;
+use PhpZip\Model\Extra\Fields\NtfsExtraField;
+
+/**
+ * Class NtfsExtraFieldTest.
+ *
+ * @internal
+ *
+ * @small
+ */
+final class NtfsExtraFieldTest extends TestCase
+{
+    protected function setUp()
+    {
+        if (\PHP_INT_SIZE === 4) {
+            self::markTestSkipped('only 64 bit test');
+        }
+    }
+
+    /**
+     * @dataProvider provideExtraField
+     *
+     * @param int    $modifyNtfsTime
+     * @param int    $accessNtfsTime
+     * @param int    $createNtfsTime
+     * @param float  $modifyTimestamp
+     * @param float  $accessTimestamp
+     * @param float  $createTimestamp
+     * @param string $binaryData
+     *
+     * @throws \Exception
+     *
+     * @noinspection PhpTooManyParametersInspection
+     */
+    public function testExtraField(
+        $modifyNtfsTime,
+        $accessNtfsTime,
+        $createNtfsTime,
+        $modifyTimestamp,
+        $accessTimestamp,
+        $createTimestamp,
+        $binaryData
+    ) {
+        $extraField = new NtfsExtraField($modifyNtfsTime, $accessNtfsTime, $createNtfsTime);
+        self::assertSame($extraField->getHeaderId(), NtfsExtraField::HEADER_ID);
+
+        self::assertEquals($extraField->getModifyDateTime()->getTimestamp(), (int) $modifyTimestamp);
+        self::assertEquals($extraField->getAccessDateTime()->getTimestamp(), (int) $accessTimestamp);
+        self::assertEquals($extraField->getCreateDateTime()->getTimestamp(), (int) $createTimestamp);
+
+        self::assertEquals(NtfsExtraField::unpackLocalFileData($binaryData), $extraField);
+        self::assertEquals(NtfsExtraField::unpackCentralDirData($binaryData), $extraField);
+
+        self::assertSame($extraField->packLocalFileData(), $binaryData);
+        self::assertSame($extraField->packCentralDirData(), $binaryData);
+
+        $extraFieldFromDateTime = NtfsExtraField::create(
+            $extraField->getModifyDateTime(),
+            $extraField->getAccessDateTime(),
+            $extraField->getCreateDateTime()
+        );
+
+        self::assertEqualsIntegerWithDelta($extraFieldFromDateTime->getModifyNtfsTime(), $extraField->getModifyNtfsTime(), 100);
+        self::assertEqualsIntegerWithDelta($extraFieldFromDateTime->getAccessNtfsTime(), $extraField->getAccessNtfsTime(), 100);
+        self::assertEqualsIntegerWithDelta($extraFieldFromDateTime->getCreateNtfsTime(), $extraField->getCreateNtfsTime(), 100);
+    }
+
+    /**
+     * @return array
+     */
+    public function provideExtraField()
+    {
+        return [
+            [
+                129853553114795379,
+                129853553114795379,
+                129853552641022547,
+                1340881711.4795379,
+                1340881711.4795379,
+                1340881664.1022547,
+                "\x00\x00\x00\x00\x01\x00\x18\x00s\xCD:Z\x1EU\xCD\x01s\xCD:Z\x1EU\xCD\x01S\x9A\xFD=\x1EU\xCD\x01",
+            ],
+            [
+                131301570250000000,
+                131865940850000000,
+                131840940680000000,
+                1485683425.000000,
+                1542120485.000000,
+                1539620468.000000,
+                "\x00\x00\x00\x00\x01\x00\x18\x00\x80\xC63\x1D\x15z\xD2\x01\x80@V\xE2_{\xD4\x01\x00\xB2\x15\x14\xA3d\xD4\x01",
+            ],
+            [
+                132181086710000000,
+                132181086710000000,
+                132181086710000000,
+                1573635071.000000,
+                1573635071.000000,
+                1573635071.000000,
+                "\x00\x00\x00\x00\x01\x00\x18\x00\x80\xE9_\x7F\xFF\x99\xD5\x01\x80\xE9_\x7F\xFF\x99\xD5\x01\x80\xE9_\x7F\xFF\x99\xD5\x01",
+            ],
+        ];
+    }
+
+    /**
+     * @param int    $expected
+     * @param int    $actual
+     * @param int    $delta
+     * @param string $message
+     */
+    private static function assertEqualsIntegerWithDelta(
+        $expected,
+        $actual,
+        $delta,
+        $message = ''
+    ) {
+        self::assertSame(
+            self::roundInt($expected, $delta),
+            self::roundInt($actual, $delta),
+            $message
+        );
+    }
+
+    /**
+     * @param int $number
+     * @param int $delta
+     *
+     * @return int
+     */
+    private static function roundInt($number, $delta)
+    {
+        return (int) (floor($number / $delta) * $delta);
+    }
+
+    /**
+     * @dataProvider provideExtraField
+     *
+     * @param int   $mtimeNtfs
+     * @param int   $atimeNtfs
+     * @param int   $ctimeNtfs
+     * @param float $mtimeTimestamp
+     * @param float $atimeTimestamp
+     * @param float $ctimeTimestamp
+     *
+     * @noinspection PhpTooManyParametersInspection
+     * @noinspection PhpUnitDeprecationsInspection
+     */
+    public function testConverter(
+        $mtimeNtfs,
+        $atimeNtfs,
+        $ctimeNtfs,
+        $mtimeTimestamp,
+        $atimeTimestamp,
+        $ctimeTimestamp
+    ) {
+        self::assertEquals(NtfsExtraField::ntfsTimeToTimestamp($mtimeNtfs), $mtimeTimestamp, '', 0.00001);
+        self::assertEquals(NtfsExtraField::ntfsTimeToTimestamp($atimeNtfs), $atimeTimestamp, '', 0.00001);
+        self::assertEquals(NtfsExtraField::ntfsTimeToTimestamp($ctimeNtfs), $ctimeTimestamp, '', 0.00001);
+
+        self::assertEqualsIntegerWithDelta(NtfsExtraField::timestampToNtfsTime($mtimeTimestamp), $mtimeNtfs, 10);
+        self::assertEqualsIntegerWithDelta(NtfsExtraField::timestampToNtfsTime($atimeTimestamp), $atimeNtfs, 10);
+        self::assertEqualsIntegerWithDelta(NtfsExtraField::timestampToNtfsTime($ctimeTimestamp), $ctimeNtfs, 10);
+    }
+
+    /**
+     * @throws \Exception
+     */
+    public function testSetter()
+    {
+        $timeZone = new \DateTimeZone('UTC');
+        $initDateTime = new \DateTimeImmutable('-1 min', $timeZone);
+        $mtimeDateTime = new \DateTimeImmutable('-1 hour', $timeZone);
+        $atimeDateTime = new \DateTimeImmutable('-1 day', $timeZone);
+        $ctimeDateTime = new \DateTimeImmutable('-1 year', $timeZone);
+
+        $extraField = NtfsExtraField::create($initDateTime, $initDateTime, $initDateTime);
+        self::assertEquals(
+            $extraField->getModifyDateTime()->getTimestamp(),
+            $initDateTime->getTimestamp()
+        );
+        self::assertEquals(
+            $extraField->getAccessDateTime()->getTimestamp(),
+            $initDateTime->getTimestamp()
+        );
+        self::assertEquals(
+            $extraField->getCreateDateTime()->getTimestamp(),
+            $initDateTime->getTimestamp()
+        );
+
+        $extraField->setModifyDateTime($mtimeDateTime);
+        self::assertEquals(
+            $extraField->getModifyDateTime()->getTimestamp(),
+            $mtimeDateTime->getTimestamp()
+        );
+        self::assertEquals(
+            $extraField->getAccessDateTime()->getTimestamp(),
+            $initDateTime->getTimestamp()
+        );
+        self::assertEquals(
+            $extraField->getCreateDateTime()->getTimestamp(),
+            $initDateTime->getTimestamp()
+        );
+
+        $extraField->setAccessDateTime($atimeDateTime);
+        self::assertEquals(
+            $extraField->getModifyDateTime()->getTimestamp(),
+            $mtimeDateTime->getTimestamp()
+        );
+        self::assertEquals(
+            $extraField->getAccessDateTime()->getTimestamp(),
+            $atimeDateTime->getTimestamp()
+        );
+        self::assertEquals(
+            $extraField->getCreateDateTime()->getTimestamp(),
+            $initDateTime->getTimestamp()
+        );
+
+        $extraField->setCreateDateTime($ctimeDateTime);
+        self::assertEquals(
+            $extraField->getModifyDateTime()->getTimestamp(),
+            $mtimeDateTime->getTimestamp()
+        );
+        self::assertEquals(
+            $extraField->getAccessDateTime()->getTimestamp(),
+            $atimeDateTime->getTimestamp()
+        );
+        self::assertEquals(
+            $extraField->getCreateDateTime()->getTimestamp(),
+            $ctimeDateTime->getTimestamp()
+        );
+
+        $newModifyNtfsTime = $extraField->getCreateNtfsTime();
+        $newAccessNtfsTime = $extraField->getModifyNtfsTime();
+        $newCreateNtfsTime = $extraField->getAccessNtfsTime();
+        $extraField->setModifyNtfsTime($newModifyNtfsTime);
+        $extraField->setAccessNtfsTime($newAccessNtfsTime);
+        $extraField->setCreateNtfsTime($newCreateNtfsTime);
+        self::assertSame($extraField->getModifyNtfsTime(), $newModifyNtfsTime);
+        self::assertSame($extraField->getAccessNtfsTime(), $newAccessNtfsTime);
+        self::assertSame($extraField->getCreateNtfsTime(), $newCreateNtfsTime);
+    }
+}

+ 156 - 0
tests/Extra/Fields/OldUnixExtraFieldTest.php

@@ -0,0 +1,156 @@
+<?php
+
+namespace PhpZip\Tests\Extra\Fields;
+
+use PHPUnit\Framework\TestCase;
+use PhpZip\Model\Extra\Fields\OldUnixExtraField;
+
+/**
+ * Class OldUnixExtraFieldTest.
+ *
+ * @internal
+ *
+ * @small
+ */
+final class OldUnixExtraFieldTest extends TestCase
+{
+    /**
+     * @dataProvider provideExtraField
+     *
+     * @param int|null $accessTime
+     * @param int|null $modifyTime
+     * @param int|null $uid
+     * @param int|null $gid
+     * @param string   $localBinaryData
+     * @param string   $cdBinaryData
+     *
+     * @noinspection PhpTooManyParametersInspection
+     *
+     * @throws \Exception
+     */
+    public function testExtraField(
+        $accessTime,
+        $modifyTime,
+        $uid,
+        $gid,
+        $localBinaryData,
+        $cdBinaryData
+    ) {
+        $extraField = new OldUnixExtraField($accessTime, $modifyTime, $uid, $gid);
+        self::assertSame($extraField->getHeaderId(), OldUnixExtraField::HEADER_ID);
+
+        self::assertSame($extraField->getAccessTime(), $accessTime);
+        self::assertSame($extraField->getModifyTime(), $modifyTime);
+        self::assertSame($extraField->getUid(), $uid);
+        self::assertSame($extraField->getGid(), $gid);
+
+        if ($extraField->getModifyTime() !== null) {
+            self::assertEquals(
+                new \DateTimeImmutable('@' . $extraField->getModifyTime()),
+                $extraField->getModifyDateTime()
+            );
+        }
+
+        if ($extraField->getAccessTime() !== null) {
+            self::assertEquals(
+                new \DateTimeImmutable('@' . $extraField->getAccessTime()),
+                $extraField->getAccessDateTime()
+            );
+        }
+
+        self::assertEquals(OldUnixExtraField::unpackLocalFileData($localBinaryData), $extraField);
+        self::assertSame($extraField->packLocalFileData(), $localBinaryData);
+
+        $uid = null;
+        $gid = null;
+        $extraField = new OldUnixExtraField($accessTime, $modifyTime, $uid, $gid);
+        self::assertSame($extraField->getHeaderId(), OldUnixExtraField::HEADER_ID);
+
+        self::assertSame($extraField->getAccessTime(), $accessTime);
+        self::assertSame($extraField->getModifyTime(), $modifyTime);
+        self::assertNull($extraField->getUid());
+        self::assertNull($extraField->getGid());
+
+        if ($extraField->getModifyTime() !== null) {
+            self::assertEquals(
+                new \DateTimeImmutable('@' . $extraField->getModifyTime()),
+                $extraField->getModifyDateTime()
+            );
+        }
+
+        if ($extraField->getAccessTime() !== null) {
+            self::assertEquals(
+                new \DateTimeImmutable('@' . $extraField->getAccessTime()),
+                $extraField->getAccessDateTime()
+            );
+        }
+
+        self::assertEquals(OldUnixExtraField::unpackCentralDirData($cdBinaryData), $extraField);
+        self::assertSame($extraField->packCentralDirData(), $cdBinaryData);
+    }
+
+    /**
+     * @return array
+     */
+    public function provideExtraField()
+    {
+        return [
+            [
+                1213373265,
+                1213365834,
+                502,
+                502,
+                "Q\x9BRHJ~RH\xF6\x01\xF6\x01",
+                "Q\x9BRHJ~RH",
+            ],
+            [
+                935520420,
+                935520401,
+                501,
+                100,
+                "\xA4\xE8\xC27\x91\xE8\xC27\xF5\x01d\x00",
+                "\xA4\xE8\xC27\x91\xE8\xC27",
+            ],
+            [
+                1402666135,
+                1402666135,
+                501,
+                20,
+                "\x97\xFC\x9AS\x97\xFC\x9AS\xF5\x01\x14\x00",
+                "\x97\xFC\x9AS\x97\xFC\x9AS",
+            ],
+            [
+                null,
+                null,
+                null,
+                null,
+                '',
+                '',
+            ],
+        ];
+    }
+
+    public function testSetter()
+    {
+        $extraField = new OldUnixExtraField(null, null, null, null);
+
+        self::assertNull($extraField->getAccessTime());
+        self::assertNull($extraField->getAccessDateTime());
+        self::assertNull($extraField->getModifyTime());
+        self::assertNull($extraField->getModifyDateTime());
+        self::assertNull($extraField->getUid());
+        self::assertNull($extraField->getGid());
+
+        $extraField->setModifyTime(1402666135);
+        self::assertSame($extraField->getModifyTime(), 1402666135);
+
+        $extraField->setAccessTime(1213365834);
+        self::assertSame($extraField->getAccessTime(), 1213365834);
+
+        $extraField->setUid(500);
+        self::assertSame($extraField->getUid(), 500);
+
+        $extraField->setGid(100);
+        self::assertSame($extraField->getGid(), 100);
+    }
+}

+ 42 - 0
tests/Extra/Fields/UnicodeCommentExtraFieldTest.php

@@ -0,0 +1,42 @@
+<?php
+
+namespace PhpZip\Tests\Extra\Fields;
+
+use PhpZip\Model\Extra\Fields\UnicodeCommentExtraField;
+
+/**
+ * @internal
+ *
+ * @small
+ */
+final class UnicodeCommentExtraFieldTest extends AbstractUnicodeExtraFieldTest
+{
+    /**
+     * {@inheritdoc}
+     */
+    protected function getUnicodeExtraFieldClassName()
+    {
+        return UnicodeCommentExtraField::class;
+    }
+
+    /**
+     * @return array
+     */
+    public function provideExtraField()
+    {
+        return [
+            [
+                4293813303,
+                'комментарий',
+                "\xAA\xAE\xAC\xAC\xA5\xAD\xE2\xA0\xE0\xA8\xA9",
+                "\x017d\xEE\xFF\xD0\xBA\xD0\xBE\xD0\xBC\xD0\xBC\xD0\xB5\xD0\xBD\xD1\x82\xD0\xB0\xD1\x80\xD0\xB8\xD0\xB9",
+            ],
+            [
+                897024324,
+                'תגובה',
+                "\x9A\x82\x85\x81\x84",
+                "\x01D\x81w5\xD7\xAA\xD7\x92\xD7\x95\xD7\x91\xD7\x94",
+            ],
+        ];
+    }
+}

+ 56 - 0
tests/Extra/Fields/UnicodePathExtraFieldTest.php

@@ -0,0 +1,56 @@
+<?php
+
+namespace PhpZip\Tests\Extra\Fields;
+
+use PhpZip\Model\Extra\Fields\UnicodePathExtraField;
+
+/**
+ * Class UnicodePathExtraFieldTest.
+ *
+ * @internal
+ *
+ * @small
+ */
+final class UnicodePathExtraFieldTest extends AbstractUnicodeExtraFieldTest
+{
+    /**
+     * {@inheritdoc}
+     */
+    protected function getUnicodeExtraFieldClassName()
+    {
+        return UnicodePathExtraField::class;
+    }
+
+    /**
+     * @return array
+     */
+    public function provideExtraField()
+    {
+        return [
+            [
+                2728523760,
+                'txt\מבחן עברי.txt',
+                "txt/\x8E\x81\x87\x8F \x92\x81\x98\x89.txt",
+                "\x01\xF0\xF7\xA1\xA2txt\\\xD7\x9E\xD7\x91\xD7\x97\xD7\x9F \xD7\xA2\xD7\x91\xD7\xA8\xD7\x99.txt",
+            ],
+            [
+                953311492,
+                'ä\ü.txt',
+                "\x84/\x81.txt",
+                "\x01\x04a\xD28\xC3\xA4\\\xC3\xBC.txt",
+            ],
+            [
+                2965532848,
+                'Ölfässer.txt',
+                "\x99lf\x84sser.txt",
+                "\x01\xB0p\xC2\xB0\xC3\x96lf\xC3\xA4sser.txt",
+            ],
+            [
+                3434671236,
+                'Как заработать в интернете.mp4',
+                "\x8A\xA0\xAA \xA7\xA0\xE0\xA0\xA1\xAE\xE2\xA0\xE2\xEC \xA2 \xA8\xAD\xE2\xA5\xE0\xAD\xA5\xE2\xA5.mp4",
+                "\x01\x84\xEC\xB8\xCC\xD0\x9A\xD0\xB0\xD0\xBA \xD0\xB7\xD0\xB0\xD1\x80\xD0\xB0\xD0\xB1\xD0\xBE\xD1\x82\xD0\xB0\xD1\x82\xD1\x8C \xD0\xB2 \xD0\xB8\xD0\xBD\xD1\x82\xD0\xB5\xD1\x80\xD0\xBD\xD0\xB5\xD1\x82\xD0\xB5.mp4",
+            ],
+        ];
+    }
+}

+ 57 - 0
tests/Extra/Fields/UnrecognizedExtraFieldTest.php

@@ -0,0 +1,57 @@
+<?php
+
+namespace PhpZip\Tests\Extra\Fields;
+
+use PHPUnit\Framework\TestCase;
+use PhpZip\Exception\RuntimeException;
+use PhpZip\Model\Extra\Fields\UnrecognizedExtraField;
+
+/**
+ * Class UnrecognizedExtraFieldTest.
+ *
+ * @internal
+ *
+ * @small
+ */
+final class UnrecognizedExtraFieldTest extends TestCase
+{
+    public function testExtraField()
+    {
+        $headerId = 0xF00D;
+        $binaryData = "\x01\x02\x03\x04\x05";
+
+        $unrecognizedExtraField = new UnrecognizedExtraField($headerId, $binaryData);
+        self::assertSame($unrecognizedExtraField->getHeaderId(), $headerId);
+        self::assertSame($unrecognizedExtraField->getData(), $binaryData);
+
+        $newHeaderId = 0xDADA;
+        $newBinaryData = "\x05\x00";
+        $unrecognizedExtraField->setHeaderId($newHeaderId);
+        self::assertSame($unrecognizedExtraField->getHeaderId(), $newHeaderId);
+        $unrecognizedExtraField->setData($newBinaryData);
+        self::assertSame($unrecognizedExtraField->getData(), $newBinaryData);
+
+        self::assertSame($unrecognizedExtraField->packLocalFileData(), $newBinaryData);
+        self::assertSame($unrecognizedExtraField->packCentralDirData(), $newBinaryData);
+    }
+
+    public function testUnpackLocalData()
+    {
+        $this->setExpectedException(
+            RuntimeException::class,
+            'Unsupport parse'
+        );
+
+        UnrecognizedExtraField::unpackLocalFileData("\x01\x02");
+    }
+
+    public function testUnpackCentralDirData()
+    {
+        $this->setExpectedException(
+            RuntimeException::class,
+            'Unsupport parse'
+        );
+
+        UnrecognizedExtraField::unpackCentralDirData("\x01\x02");
+    }
+}

+ 240 - 0
tests/Extra/Fields/WinZipAesExtraFieldTest.php

@@ -0,0 +1,240 @@
+<?php
+
+namespace PhpZip\Tests\Extra\Fields;
+
+use PHPUnit\Framework\TestCase;
+use PhpZip\Constants\ZipCompressionMethod;
+use PhpZip\Constants\ZipEncryptionMethod;
+use PhpZip\Exception\InvalidArgumentException;
+use PhpZip\Exception\ZipException;
+use PhpZip\Exception\ZipUnsupportMethodException;
+use PhpZip\Model\Extra\Fields\WinZipAesExtraField;
+
+/**
+ * @internal
+ *
+ * @small
+ */
+final class WinZipAesExtraFieldTest extends TestCase
+{
+    /**
+     * @dataProvider provideExtraField
+     *
+     * @param int    $vendorVersion
+     * @param int    $keyStrength
+     * @param int    $compressionMethod
+     * @param int    $saltSize
+     * @param string $binaryData
+     *
+     * @throws ZipException
+     * @throws ZipUnsupportMethodException
+     */
+    public function testExtraField(
+        $vendorVersion,
+        $keyStrength,
+        $compressionMethod,
+        $saltSize,
+        $binaryData
+    ) {
+        $extraField = new WinZipAesExtraField($vendorVersion, $keyStrength, $compressionMethod);
+        self::assertSame($extraField->getHeaderId(), WinZipAesExtraField::HEADER_ID);
+        self::assertSame($extraField->getVendorVersion(), $vendorVersion);
+        self::assertSame($extraField->getKeyStrength(), $keyStrength);
+        self::assertSame($extraField->getCompressionMethod(), $compressionMethod);
+        self::assertSame($extraField->getVendorId(), WinZipAesExtraField::VENDOR_ID);
+        self::assertSame($extraField->getSaltSize(), $saltSize);
+
+        self::assertSame($binaryData, $extraField->packLocalFileData());
+        self::assertSame($binaryData, $extraField->packCentralDirData());
+        self::assertEquals(WinZipAesExtraField::unpackLocalFileData($binaryData), $extraField);
+        self::assertEquals(WinZipAesExtraField::unpackCentralDirData($binaryData), $extraField);
+    }
+
+    /**
+     * @return array
+     */
+    public function provideExtraField()
+    {
+        return [
+            [
+                WinZipAesExtraField::VERSION_AE1,
+                WinZipAesExtraField::KEY_STRENGTH_128BIT,
+                ZipCompressionMethod::STORED,
+                8,
+                "\x01\x00AE\x01\x00\x00",
+            ],
+            [
+                WinZipAesExtraField::VERSION_AE1,
+                WinZipAesExtraField::KEY_STRENGTH_192BIT,
+                ZipCompressionMethod::DEFLATED,
+                12,
+                "\x01\x00AE\x02\x08\x00",
+            ],
+            [
+                WinZipAesExtraField::VERSION_AE2,
+                WinZipAesExtraField::KEY_STRENGTH_128BIT,
+                ZipCompressionMethod::DEFLATED,
+                8,
+                "\x02\x00AE\x01\x08\x00",
+            ],
+            [
+                WinZipAesExtraField::VERSION_AE2,
+                WinZipAesExtraField::KEY_STRENGTH_256BIT,
+                ZipCompressionMethod::STORED,
+                16,
+                "\x02\x00AE\x03\x00\x00",
+            ],
+            [
+                WinZipAesExtraField::VERSION_AE2,
+                WinZipAesExtraField::KEY_STRENGTH_192BIT,
+                ZipCompressionMethod::DEFLATED,
+                12,
+                "\x02\x00AE\x02\x08\x00",
+            ],
+            [
+                WinZipAesExtraField::VERSION_AE2,
+                WinZipAesExtraField::KEY_STRENGTH_256BIT,
+                ZipCompressionMethod::STORED,
+                16,
+                "\x02\x00AE\x03\x00\x00",
+            ],
+        ];
+    }
+
+    /**
+     * @throws ZipUnsupportMethodException
+     */
+    public function testSetter()
+    {
+        $extraField = new WinZipAesExtraField(
+            WinZipAesExtraField::VERSION_AE1,
+            WinZipAesExtraField::KEY_STRENGTH_256BIT,
+            ZipCompressionMethod::DEFLATED
+        );
+
+        self::assertSame($extraField->getVendorVersion(), WinZipAesExtraField::VERSION_AE1);
+        self::assertSame($extraField->getKeyStrength(), WinZipAesExtraField::KEY_STRENGTH_256BIT);
+        self::assertSame($extraField->getCompressionMethod(), ZipCompressionMethod::DEFLATED);
+        self::assertSame($extraField->getSaltSize(), 16);
+        self::assertSame($extraField->getEncryptionStrength(), 256);
+        self::assertSame($extraField->getEncryptionMethod(), ZipEncryptionMethod::WINZIP_AES_256);
+
+        $extraField->setVendorVersion(WinZipAesExtraField::VERSION_AE2);
+        self::assertSame($extraField->getVendorVersion(), WinZipAesExtraField::VERSION_AE2);
+        self::assertSame($extraField->getKeyStrength(), WinZipAesExtraField::KEY_STRENGTH_256BIT);
+        self::assertSame($extraField->getCompressionMethod(), ZipCompressionMethod::DEFLATED);
+        self::assertSame($extraField->getSaltSize(), 16);
+        self::assertSame($extraField->getEncryptionStrength(), 256);
+        self::assertSame($extraField->getEncryptionMethod(), ZipEncryptionMethod::WINZIP_AES_256);
+
+        $extraField->setKeyStrength(WinZipAesExtraField::KEY_STRENGTH_128BIT);
+        self::assertSame($extraField->getVendorVersion(), WinZipAesExtraField::VERSION_AE2);
+        self::assertSame($extraField->getKeyStrength(), WinZipAesExtraField::KEY_STRENGTH_128BIT);
+        self::assertSame($extraField->getCompressionMethod(), ZipCompressionMethod::DEFLATED);
+        self::assertSame($extraField->getSaltSize(), 8);
+        self::assertSame($extraField->getEncryptionStrength(), 128);
+        self::assertSame($extraField->getEncryptionMethod(), ZipEncryptionMethod::WINZIP_AES_128);
+
+        $extraField->setKeyStrength(WinZipAesExtraField::KEY_STRENGTH_192BIT);
+        self::assertSame($extraField->getVendorVersion(), WinZipAesExtraField::VERSION_AE2);
+        self::assertSame($extraField->getKeyStrength(), WinZipAesExtraField::KEY_STRENGTH_192BIT);
+        self::assertSame($extraField->getCompressionMethod(), ZipCompressionMethod::DEFLATED);
+        self::assertSame($extraField->getSaltSize(), 12);
+        self::assertSame($extraField->getEncryptionStrength(), 192);
+        self::assertSame($extraField->getEncryptionMethod(), ZipEncryptionMethod::WINZIP_AES_192);
+
+        $extraField->setCompressionMethod(ZipCompressionMethod::STORED);
+        self::assertSame($extraField->getVendorVersion(), WinZipAesExtraField::VERSION_AE2);
+        self::assertSame($extraField->getKeyStrength(), WinZipAesExtraField::KEY_STRENGTH_192BIT);
+        self::assertSame($extraField->getCompressionMethod(), ZipCompressionMethod::STORED);
+        self::assertSame($extraField->getSaltSize(), 12);
+        self::assertSame($extraField->getEncryptionStrength(), 192);
+        self::assertSame($extraField->getEncryptionMethod(), ZipEncryptionMethod::WINZIP_AES_192);
+    }
+
+    /**
+     * @throws ZipUnsupportMethodException
+     */
+    public function testConstructUnsupportVendorVersion()
+    {
+        $this->setExpectedException(InvalidArgumentException::class, 'Unsupport WinZip AES vendor version: 3');
+
+        new WinZipAesExtraField(
+            3,
+            WinZipAesExtraField::KEY_STRENGTH_192BIT,
+            ZipCompressionMethod::STORED
+        );
+    }
+
+    /**
+     * @throws ZipUnsupportMethodException
+     */
+    public function testSetterUnsupportVendorVersion()
+    {
+        $this->setExpectedException(InvalidArgumentException::class, 'Unsupport WinZip AES vendor version: 3');
+
+        $extraField = new WinZipAesExtraField(
+            WinZipAesExtraField::VERSION_AE1,
+            WinZipAesExtraField::KEY_STRENGTH_192BIT,
+            ZipCompressionMethod::STORED
+        );
+        $extraField->setVendorVersion(3);
+    }
+
+    /**
+     * @throws ZipUnsupportMethodException
+     */
+    public function testConstructUnsupportCompressionMethod()
+    {
+        $this->setExpectedException(ZipUnsupportMethodException::class, 'Compression method 3 (Reduced compression factor 2) is not supported.');
+
+        new WinZipAesExtraField(
+            WinZipAesExtraField::VERSION_AE1,
+            WinZipAesExtraField::KEY_STRENGTH_192BIT,
+            3
+        );
+    }
+
+    /**
+     * @throws ZipUnsupportMethodException
+     */
+    public function testSetterUnsupportCompressionMethod()
+    {
+        $this->setExpectedException(ZipUnsupportMethodException::class, 'Compression method 3 (Reduced compression factor 2) is not supported.');
+
+        $extraField = new WinZipAesExtraField(
+            WinZipAesExtraField::VERSION_AE1,
+            WinZipAesExtraField::KEY_STRENGTH_192BIT,
+            ZipCompressionMethod::STORED
+        );
+        $extraField->setCompressionMethod(3);
+    }
+
+    /**
+     * @throws ZipUnsupportMethodException
+     */
+    public function testConstructUnsupportKeyStrength()
+    {
+        $this->setExpectedException(InvalidArgumentException::class, 'Key strength 16 not support value. Allow values: 1, 2, 3');
+
+        new WinZipAesExtraField(
+            WinZipAesExtraField::VERSION_AE1,
+            0x10,
+            ZipCompressionMethod::STORED
+        );
+    }
+
+    /**
+     * @throws ZipUnsupportMethodException
+     */
+    public function testSetterUnsupportKeyStrength()
+    {
+        $this->setExpectedException(InvalidArgumentException::class, 'Key strength 16 not support value. Allow values: 1, 2, 3');
+
+        new WinZipAesExtraField(
+            WinZipAesExtraField::VERSION_AE1,
+            0x10,
+            ZipCompressionMethod::STORED
+        );
+    }
+}

+ 132 - 0
tests/Extra/Fields/Zip64ExtraFieldTest.php

@@ -0,0 +1,132 @@
+<?php
+
+namespace PhpZip\Tests\Extra\Fields;
+
+use PHPUnit\Framework\TestCase;
+use PhpZip\Constants\ZipConstants;
+use PhpZip\Exception\ZipException;
+use PhpZip\Model\Extra\Fields\Zip64ExtraField;
+use PhpZip\Model\ZipEntry;
+
+/**
+ * @internal
+ *
+ * @small
+ */
+final class Zip64ExtraFieldTest extends TestCase
+{
+    protected function setUp()
+    {
+        if (\PHP_INT_SIZE === 4) {
+            self::markTestSkipped('only 64 bit test');
+        }
+    }
+
+    /**
+     * @dataProvider provideExtraField
+     *
+     * @param int|null    $uncompressedSize
+     * @param int|null    $compressedSize
+     * @param int|null    $localHeaderOffset
+     * @param int|null    $diskStart
+     * @param string|null $localBinaryData
+     * @param string|null $cdBinaryData
+     *
+     * @throws ZipException
+     *
+     * @noinspection PhpTooManyParametersInspection
+     */
+    public function testExtraField(
+        $uncompressedSize,
+        $compressedSize,
+        $localHeaderOffset,
+        $diskStart,
+        $localBinaryData,
+        $cdBinaryData
+    ) {
+        $extraField = new Zip64ExtraField(
+            $uncompressedSize,
+            $compressedSize,
+            $localHeaderOffset,
+            $diskStart
+        );
+        self::assertSame($extraField->getHeaderId(), Zip64ExtraField::HEADER_ID);
+        self::assertSame($extraField->getUncompressedSize(), $uncompressedSize);
+        self::assertSame($extraField->getCompressedSize(), $compressedSize);
+        self::assertSame($extraField->getLocalHeaderOffset(), $localHeaderOffset);
+        self::assertSame($extraField->getDiskStart(), $diskStart);
+
+        $zipEntry = new ZipEntry('entry');
+        $zipEntry->setUncompressedSize($uncompressedSize !== null ? ZipConstants::ZIP64_MAGIC : 0xfffff);
+        $zipEntry->setCompressedSize($compressedSize !== null ? ZipConstants::ZIP64_MAGIC : 0xffff);
+        $zipEntry->setLocalHeaderOffset($localHeaderOffset !== null ? ZipConstants::ZIP64_MAGIC : 0xfff);
+
+        if ($localBinaryData !== null) {
+            self::assertSame($localBinaryData, $extraField->packLocalFileData());
+            self::assertEquals(Zip64ExtraField::unpackLocalFileData($localBinaryData, $zipEntry), $extraField);
+        }
+
+        if ($cdBinaryData !== null) {
+            self::assertSame($cdBinaryData, $extraField->packCentralDirData());
+            self::assertEquals(Zip64ExtraField::unpackCentralDirData($cdBinaryData, $zipEntry), $extraField);
+        }
+    }
+
+    /**
+     * @return array
+     */
+    public function provideExtraField()
+    {
+        return [
+            [
+                0,
+                2,
+                null,
+                null,
+                "\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00",
+                null,
+            ],
+            [
+                5368709120,
+                5369580144,
+                null,
+                null,
+                null,
+                "\x00\x00\x00@\x01\x00\x00\x00pJ\x0D@\x01\x00\x00\x00",
+            ],
+            [
+                null,
+                null,
+                4945378839,
+                null,
+                null,
+                "\x17~\xC4&\x01\x00\x00\x00",
+            ],
+        ];
+    }
+
+    public function testSetter()
+    {
+        $extraField = new Zip64ExtraField();
+        self::assertNull($extraField->getUncompressedSize());
+        self::assertNull($extraField->getCompressedSize());
+        self::assertNull($extraField->getLocalHeaderOffset());
+        self::assertNull($extraField->getDiskStart());
+
+        $uncompressedSize = 12222;
+        $extraField->setUncompressedSize($uncompressedSize);
+        self::assertSame($extraField->getUncompressedSize(), $uncompressedSize);
+
+        $compressedSize = 12222;
+        $extraField->setCompressedSize($uncompressedSize);
+        self::assertSame($extraField->getCompressedSize(), $compressedSize);
+
+        $localHeaderOffset = 12222;
+        $extraField->setLocalHeaderOffset($localHeaderOffset);
+        self::assertSame($extraField->getLocalHeaderOffset(), $localHeaderOffset);
+
+        $diskStart = 2;
+        $extraField->setDiskStart($diskStart);
+        self::assertSame($extraField->getDiskStart(), $diskStart);
+    }
+}

+ 39 - 0
tests/Internal/CustomZip/CustomZipWriter.php

@@ -0,0 +1,39 @@
+<?php
+
+namespace PhpZip\Tests\Internal\CustomZip;
+
+use PhpZip\IO\ZipWriter;
+use PhpZip\Model\Extra\Fields\NewUnixExtraField;
+use PhpZip\Model\Extra\Fields\NtfsExtraField;
+use PhpZip\Model\ZipContainer;
+
+/**
+ * Class CustomZipWriter.
+ */
+class CustomZipWriter extends ZipWriter
+{
+    /**
+     * ZipWriter constructor.
+     *
+     * @param ZipContainer $container
+     */
+    public function __construct(ZipContainer $container)
+    {
+//        dump($container);
+        parent::__construct($container);
+//        dd($this->zipContainer);
+    }
+
+    protected function beforeWrite()
+    {
+        parent::beforeWrite();
+        $now = new \DateTimeImmutable();
+        $ntfsTimeExtra = NtfsExtraField::create($now, $now->modify('-1 day'), $now->modify('-10 day'));
+        $unixExtra = new NewUnixExtraField();
+
+        foreach ($this->zipContainer->getEntries() as $entry) {
+            $entry->addExtraField($ntfsTimeExtra);
+            $entry->addExtraField($unixExtra);
+        }
+    }
+}

+ 20 - 0
tests/Internal/CustomZip/ZipFileCustomWriter.php

@@ -0,0 +1,20 @@
+<?php
+
+namespace PhpZip\Tests\Internal\CustomZip;
+
+use PhpZip\IO\ZipWriter;
+use PhpZip\ZipFile;
+
+/**
+ * Class ZipFileCustomWriter.
+ */
+class ZipFileCustomWriter extends ZipFile
+{
+    /**
+     * @return ZipWriter
+     */
+    protected function createZipWriter()
+    {
+        return new CustomZipWriter($this->zipContainer);
+    }
+}

+ 28 - 0
tests/Internal/CustomZip/ZipFileWithBeforeSave.php

@@ -0,0 +1,28 @@
+<?php
+
+namespace PhpZip\Tests\Internal\CustomZip;
+
+use PhpZip\Model\Extra\Fields\NewUnixExtraField;
+use PhpZip\Model\Extra\Fields\NtfsExtraField;
+use PhpZip\ZipFile;
+
+/**
+ * Class ZipFileWithBeforeSave.
+ */
+class ZipFileWithBeforeSave extends ZipFile
+{
+    /**
+     * Event before save or output.
+     */
+    protected function onBeforeSave()
+    {
+        $now = new \DateTimeImmutable();
+        $ntfsTimeExtra = NtfsExtraField::create($now, $now->modify('-1 day'), $now->modify('-10 day'));
+        $unixExtra = new NewUnixExtraField();
+
+        foreach ($this->zipContainer->getEntries() as $entry) {
+            $entry->addExtraField($ntfsTimeExtra);
+            $entry->addExtraField($unixExtra);
+        }
+    }
+}

+ 98 - 0
tests/Internal/Epub/EpubFile.php

@@ -0,0 +1,98 @@
+<?php
+
+/** @noinspection PhpComposerExtensionStubsInspection */
+
+namespace PhpZip\Tests\Internal\Epub;
+
+use PhpZip\Constants\ZipPlatform;
+use PhpZip\Exception\ZipEntryNotFoundException;
+use PhpZip\Exception\ZipException;
+use PhpZip\IO\ZipReader;
+use PhpZip\IO\ZipWriter;
+use PhpZip\Model\ImmutableZipContainer;
+use PhpZip\Model\ZipContainer;
+use PhpZip\Model\ZipEntry;
+use PhpZip\ZipFile;
+
+/**
+ * Class EpubFile.
+ *
+ * @property EpubZipContainer $zipContainer
+ */
+class EpubFile extends ZipFile
+{
+    /**
+     * @return ZipWriter
+     */
+    protected function createZipWriter()
+    {
+        return new EpubWriter($this->zipContainer);
+    }
+
+    /**
+     * @param resource $inputStream
+     * @param array    $options
+     *
+     * @return ZipReader
+     */
+    protected function createZipReader($inputStream, array $options = [])
+    {
+        return new EpubReader($inputStream, $options);
+    }
+
+    /**
+     * @param ImmutableZipContainer|null $sourceContainer
+     *
+     * @return ZipContainer
+     */
+    protected function createZipContainer(ImmutableZipContainer $sourceContainer = null)
+    {
+        return new EpubZipContainer($sourceContainer);
+    }
+
+    /**
+     * @param ZipEntry $zipEntry
+     */
+    protected function addZipEntry(ZipEntry $zipEntry)
+    {
+        $zipEntry->setCreatedOS(ZipPlatform::OS_DOS);
+        $zipEntry->setExtractedOS(ZipPlatform::OS_UNIX);
+        parent::addZipEntry($zipEntry);
+    }
+
+    /**
+     * @throws ZipEntryNotFoundException
+     *
+     * @return string
+     */
+    public function getMimeType()
+    {
+        return $this->zipContainer->getMimeType();
+    }
+
+    public function getEpubInfo()
+    {
+        return new EpubInfo($this->getEntryContents($this->getRootFile()));
+    }
+
+    /**
+     * @throws ZipException
+     *
+     * @return string
+     */
+    public function getRootFile()
+    {
+        $entryName = 'META-INF/container.xml';
+        $contents = $this->getEntryContents($entryName);
+        $doc = new \DOMDocument();
+        $doc->loadXML($contents);
+        $xpath = new \DOMXPath($doc);
+        $rootFile = $xpath->evaluate('string(//@full-path)');
+
+        if ($rootFile === '') {
+            throw new ZipException('Incorrect ' . $entryName . ' file format');
+        }
+
+        return $rootFile;
+    }
+}

+ 159 - 0
tests/Internal/Epub/EpubInfo.php

@@ -0,0 +1,159 @@
+<?php
+
+/** @noinspection PhpComposerExtensionStubsInspection */
+
+namespace PhpZip\Tests\Internal\Epub;
+
+use PhpZip\Exception\ZipException;
+
+/**
+ * Class EpubInfo.
+ *
+ * @see http://idpf.org/epub/30/spec/epub30-publications.html
+ */
+class EpubInfo
+{
+    /** @var string|null */
+    private $title;
+
+    /** @var string|null */
+    private $creator;
+
+    /** @var string|null */
+    private $language;
+
+    /** @var string|null */
+    private $publisher;
+
+    /** @var string|null */
+    private $description;
+
+    /** @var string|null */
+    private $rights;
+
+    /** @var string|null */
+    private $date;
+
+    /** @var string|null */
+    private $subject;
+
+    /**
+     * EpubInfo constructor.
+     *
+     * @param $xmlContents
+     *
+     * @throws ZipException
+     */
+    public function __construct($xmlContents)
+    {
+        $doc = new \DOMDocument();
+        $doc->loadXML($xmlContents);
+        $xpath = new \DOMXpath($doc);
+        $xpath->registerNamespace('root', 'http://www.idpf.org/2007/opf');
+        $metaDataNodeList = $xpath->query('//root:metadata');
+
+        if (\count($metaDataNodeList) !== 1) {
+            throw new ZipException('Invalid .opf file format');
+        }
+        $metaDataNode = $metaDataNodeList->item(0);
+
+        $title = $xpath->evaluate('string(//dc:title)', $metaDataNode);
+        $creator = $xpath->evaluate('string(//dc:creator)', $metaDataNode);
+        $language = $xpath->evaluate('string(//dc:language)', $metaDataNode);
+        $publisher = $xpath->evaluate('string(//dc:publisher)', $metaDataNode);
+        $description = $xpath->evaluate('string(//dc:description)', $metaDataNode);
+        $rights = $xpath->evaluate('string(//dc:rights)', $metaDataNode);
+        $date = $xpath->evaluate('string(//dc:date)', $metaDataNode);
+        $subject = $xpath->evaluate('string(//dc:subject)', $metaDataNode);
+
+        $this->title = empty($title) ? null : $title;
+        $this->creator = empty($creator) ? null : $creator;
+        $this->language = empty($language) ? null : $language;
+        $this->publisher = empty($publisher) ? null : $publisher;
+        $this->description = empty($description) ? null : $description;
+        $this->rights = empty($rights) ? null : $rights;
+        $this->date = empty($date) ? null : $date;
+        $this->subject = empty($subject) ? null : $subject;
+    }
+
+    /**
+     * @return string|null
+     */
+    public function getTitle()
+    {
+        return $this->title;
+    }
+
+    /**
+     * @return string|null
+     */
+    public function getCreator()
+    {
+        return $this->creator;
+    }
+
+    /**
+     * @return string|null
+     */
+    public function getLanguage()
+    {
+        return $this->language;
+    }
+
+    /**
+     * @return string|null
+     */
+    public function getPublisher()
+    {
+        return $this->publisher;
+    }
+
+    /**
+     * @return string|null
+     */
+    public function getDescription()
+    {
+        return $this->description;
+    }
+
+    /**
+     * @return string|null
+     */
+    public function getRights()
+    {
+        return $this->rights;
+    }
+
+    /**
+     * @return string|null
+     */
+    public function getDate()
+    {
+        return $this->date;
+    }
+
+    /**
+     * @return string|null
+     */
+    public function getSubject()
+    {
+        return $this->subject;
+    }
+
+    /**
+     * @return array
+     */
+    public function toArray()
+    {
+        return [
+            'title' => $this->title,
+            'creator' => $this->creator,
+            'language' => $this->language,
+            'publisher' => $this->publisher,
+            'description' => $this->description,
+            'rights' => $this->rights,
+            'date' => $this->date,
+            'subject' => $this->subject,
+        ];
+    }
+}

+ 21 - 0
tests/Internal/Epub/EpubReader.php

@@ -0,0 +1,21 @@
+<?php
+
+namespace PhpZip\Tests\Internal\Epub;
+
+use PhpZip\IO\ZipReader;
+
+/**
+ * Class EpubReader.
+ */
+class EpubReader extends ZipReader
+{
+    /**
+     * @return bool
+     *
+     * @see https://github.com/w3c/epubcheck/issues/334
+     */
+    protected function isZip64Support()
+    {
+        return false;
+    }
+}

+ 66 - 0
tests/Internal/Epub/EpubWriter.php

@@ -0,0 +1,66 @@
+<?php
+
+namespace PhpZip\Tests\Internal\Epub;
+
+use PhpZip\Constants\ZipCompressionMethod;
+use PhpZip\Constants\ZipPlatform;
+use PhpZip\Exception\ZipUnsupportMethodException;
+use PhpZip\IO\ZipWriter;
+use PhpZip\Model\Data\ZipNewData;
+use PhpZip\Model\ZipEntry;
+
+/**
+ * Class EpubWriter.
+ *
+ * @property EpubZipContainer $zipContainer
+ */
+class EpubWriter extends ZipWriter
+{
+    /**
+     * @throws ZipUnsupportMethodException
+     */
+    protected function beforeWrite()
+    {
+        parent::beforeWrite();
+
+        if (!$this->zipContainer->hasEntry('mimetype')) {
+            $zipEntry = new ZipEntry('mimetype');
+            $zipEntry->setCreatedOS(ZipPlatform::OS_DOS);
+            $zipEntry->setExtractedOS(ZipPlatform::OS_DOS);
+            $zipEntry->setCompressionMethod(ZipCompressionMethod::STORED);
+            $zipEntry->setData(new ZipNewData($zipEntry, 'application/epub+zip'));
+            $this->zipContainer->addEntry($zipEntry);
+        }
+
+        $this->sortEntries();
+    }
+
+    private function sortEntries()
+    {
+        $this->zipContainer->sortByEntry(
+            static function (ZipEntry $a, ZipEntry $b) {
+                if (strcasecmp($a->getName(), 'mimetype') === 0) {
+                    return -1;
+                }
+
+                if (strcasecmp($b->getName(), 'mimetype') === 0) {
+                    return 1;
+                }
+
+                if ($a->isDirectory() && $b->isDirectory()) {
+                    return strcmp($a->getName(), $b->getName());
+                }
+
+                if ($a->isDirectory()) {
+                    return -1;
+                }
+
+                if ($b->isDirectory()) {
+                    return 1;
+                }
+
+                return strcmp($a->getName(), $b->getName());
+            }
+        );
+    }
+}

+ 22 - 0
tests/Internal/Epub/EpubZipContainer.php

@@ -0,0 +1,22 @@
+<?php
+
+namespace PhpZip\Tests\Internal\Epub;
+
+use PhpZip\Exception\ZipEntryNotFoundException;
+use PhpZip\Model\ZipContainer;
+
+/**
+ * Class EpubZipContainer.
+ */
+class EpubZipContainer extends ZipContainer
+{
+    /**
+     * @throws ZipEntryNotFoundException
+     *
+     * @return string
+     */
+    public function getMimeType()
+    {
+        return $this->getEntry('mimetype')->getData()->getDataAsString();
+    }
+}

+ 88 - 0
tests/SymlinkTest.php

@@ -0,0 +1,88 @@
+<?php
+
+namespace PhpZip\Tests;
+
+use PhpZip\Constants\ZipOptions;
+use PhpZip\Util\FilesUtil;
+use PhpZip\ZipFile;
+use Symfony\Component\Finder\Finder;
+
+/**
+ * @internal
+ *
+ * @small
+ */
+final class SymlinkTest extends ZipFileTest
+{
+    /**
+     * This method is called before the first test of this test class is run.
+     */
+    public static function setUpBeforeClass()
+    {
+        parent::setUpBeforeClass();
+
+        if (\DIRECTORY_SEPARATOR === '\\') {
+            self::markTestSkipped('only linux test');
+
+            return;
+        }
+    }
+
+    /**
+     * @dataProvider provideAllowSymlink
+     *
+     * @param bool $allowSymlink
+     *
+     * @throws \Exception
+     */
+    public function testSymlink($allowSymlink)
+    {
+        if (!is_dir($this->outputDirname)) {
+            self::assertTrue(mkdir($this->outputDirname, 0755, true));
+        }
+
+        $contentsFile = random_bytes(100);
+        $filePath = $this->outputDirname . '/file.bin';
+        $symlinkPath = $this->outputDirname . '/symlink.bin';
+        $symlinkTarget = basename($filePath);
+        self::assertNotFalse(file_put_contents($filePath, $contentsFile));
+        self::assertTrue(symlink($symlinkTarget, $symlinkPath));
+
+        $finder = (new Finder())->in($this->outputDirname);
+        $zipFile = new ZipFile();
+        $zipFile->addFromFinder($finder);
+        $zipFile->saveAsFile($this->outputFilename);
+        $zipFile->close();
+
+        self::assertCorrectZipArchive($this->outputFilename);
+
+        FilesUtil::removeDir($this->outputDirname);
+        self::assertFalse(is_dir($this->outputDirname));
+        self::assertTrue(mkdir($this->outputDirname, 0755, true));
+
+        $zipFile->openFile($this->outputFilename);
+        $zipFile->extractTo($this->outputDirname, null, [
+            ZipOptions::EXTRACT_SYMLINKS => $allowSymlink,
+        ]);
+        $zipFile->close();
+
+        $splFileInfo = new \SplFileInfo($symlinkPath);
+
+        if ($allowSymlink) {
+            self::assertTrue($splFileInfo->isLink());
+            self::assertSame($splFileInfo->getLinkTarget(), $symlinkTarget);
+        } else {
+            self::assertFalse($splFileInfo->isLink());
+            self::assertStringEqualsFile($symlinkPath, $symlinkTarget);
+        }
+    }
+
+    /**
+     * @return \Generator
+     */
+    public function provideAllowSymlink()
+    {
+        yield 'allow' => [true];
+        yield 'deny' => [false];
+    }
+}

+ 2 - 1
tests/Zip64Test.php

@@ -2,6 +2,7 @@
 
 namespace PhpZip\Tests;
 
+use PhpZip\Constants\ZipCompressionMethod;
 use PhpZip\Exception\ZipException;
 use PhpZip\ZipFile;
 
@@ -32,7 +33,7 @@ class Zip64Test extends ZipTestCase
 
         $zipFile = new ZipFile();
         for ($i = 0; $i < $countFiles; $i++) {
-            $zipFile[$i . '.txt'] = (string) $i;
+            $zipFile->addFromString($i . '.txt', (string) $i, ZipCompressionMethod::STORED);
         }
         $zipFile->saveAsFile($this->outputFilename);
         $zipFile->close();

+ 49 - 0
tests/ZipEntryTest.php

@@ -304,6 +304,55 @@ class ZipEntryTest extends TestCase
         static::assertNull($zipEntry->getData());
     }
 
+    /**
+     * @throws \Exception
+     */
+    public function testZipNewDataGuardClone()
+    {
+        $resource = fopen('php://temp', 'r+b');
+        static::assertNotFalse($resource);
+        fwrite($resource, random_bytes(1024));
+        rewind($resource);
+
+        $zipEntry = new ZipEntry('entry');
+        $zipEntry2 = new ZipEntry('entry2');
+
+        $zipData = new ZipNewData($zipEntry, $resource);
+        $zipData2 = new ZipNewData($zipEntry2, $resource);
+        $cloneData = clone $zipData;
+        $cloneData2 = clone $cloneData;
+
+        static::assertSame($zipData->getDataAsStream(), $resource);
+        static::assertSame($zipData2->getDataAsStream(), $resource);
+        static::assertSame($cloneData->getDataAsStream(), $resource);
+        static::assertSame($cloneData2->getDataAsStream(), $resource);
+
+        $validResource = \is_resource($resource);
+        static::assertTrue($validResource);
+
+        unset($cloneData);
+        $validResource = \is_resource($resource);
+        static::assertTrue($validResource);
+
+        unset($zipData);
+        $validResource = \is_resource($resource);
+        static::assertTrue($validResource);
+
+        unset($zipData2);
+        $validResource = \is_resource($resource);
+        static::assertTrue($validResource);
+
+        $reflectionClass = new \ReflectionClass($cloneData2);
+        static::assertSame(
+            $reflectionClass->getStaticProperties()['guardClonedStream'][(int) $resource],
+            0
+        );
+
+        unset($cloneData2);
+        $validResource = \is_resource($resource);
+        static::assertFalse($validResource);
+    }
+
     /**
      * @dataProvider providePlatform
      *

+ 0 - 41
tests/ZipEventTest.php

@@ -1,41 +0,0 @@
-<?php
-
-namespace PhpZip\Tests;
-
-use PhpZip\Exception\ZipException;
-use PhpZip\Tests\Internal\ZipFileExtended;
-
-/**
- * @internal
- *
- * @small
- */
-class ZipEventTest extends ZipTestCase
-{
-    /**
-     * @throws ZipException
-     */
-    public function testBeforeSave()
-    {
-        $zipFile = new ZipFileExtended();
-        $zipFile->openFile(__DIR__ . '/resources/apk.zip');
-        static::assertTrue(isset($zipFile['META-INF/MANIFEST.MF']));
-        static::assertTrue(isset($zipFile['META-INF/CERT.SF']));
-        static::assertTrue(isset($zipFile['META-INF/CERT.RSA']));
-        // the "META-INF/" folder will be deleted when saved
-        // in the ZipFileExtended::onBeforeSave() method
-        $zipFile->saveAsFile($this->outputFilename);
-        static::assertFalse(isset($zipFile['META-INF/MANIFEST.MF']));
-        static::assertFalse(isset($zipFile['META-INF/CERT.SF']));
-        static::assertFalse(isset($zipFile['META-INF/CERT.RSA']));
-        $zipFile->close();
-
-        static::assertCorrectZipArchive($this->outputFilename);
-
-        $zipFile->openFile($this->outputFilename);
-        static::assertFalse(isset($zipFile['META-INF/MANIFEST.MF']));
-        static::assertFalse(isset($zipFile['META-INF/CERT.SF']));
-        static::assertFalse(isset($zipFile['META-INF/CERT.RSA']));
-        $zipFile->close();
-    }
-}

+ 70 - 8
tests/ZipFileTest.php

@@ -1010,16 +1010,16 @@ class ZipFileTest extends ZipTestCase
             'test1.txt' => random_bytes(255),
             'test2.txt' => random_bytes(255),
             'test/test 2/test3.txt' => random_bytes(255),
-            'test empty/dir' => null,
+            'test empty/dir/' => null,
         ];
 
         $zipFile = new ZipFile();
 
-        foreach ($entries as $entryName => $value) {
-            if ($value === null) {
+        foreach ($entries as $entryName => $contents) {
+            if ($contents === null) {
                 $zipFile->addEmptyDir($entryName);
             } else {
-                $zipFile->addFromString($entryName, $value);
+                $zipFile->addFromString($entryName, $contents);
             }
         }
         $zipFile->saveAsFile($this->outputFilename);
@@ -1028,19 +1028,28 @@ class ZipFileTest extends ZipTestCase
         static::assertTrue(mkdir($this->outputDirname, 0755, true));
 
         $zipFile->openFile($this->outputFilename);
-        $zipFile->extractTo($this->outputDirname);
+        $zipFile->extractTo($this->outputDirname, null, [], $extractedEntries);
 
-        foreach ($entries as $entryName => $value) {
+        foreach ($entries as $entryName => $contents) {
             $fullExtractedFilename = $this->outputDirname . \DIRECTORY_SEPARATOR . $entryName;
 
-            if ($value === null) {
+            static::assertTrue(
+                isset($extractedEntries[$fullExtractedFilename]),
+                'No extract info for ' . $fullExtractedFilename
+            );
+
+            if ($contents === null) {
                 static::assertTrue(is_dir($fullExtractedFilename));
                 static::assertTrue(FilesUtil::isEmptyDir($fullExtractedFilename));
             } else {
                 static::assertTrue(is_file($fullExtractedFilename));
                 $contents = file_get_contents($fullExtractedFilename);
-                static::assertSame($contents, $value);
+                static::assertSame($contents, $contents);
             }
+
+            /** @var ZipEntry $entry */
+            $entry = $extractedEntries[$fullExtractedFilename];
+            static::assertSame($entry->getName(), $entryName);
         }
         $zipFile->close();
     }
@@ -2420,6 +2429,59 @@ class ZipFileTest extends ZipTestCase
         $zipFile->close();
     }
 
+    /**
+     * @throws ZipEntryNotFoundException
+     * @throws ZipException
+     */
+    public function testCloneZipContainerInZipWriter()
+    {
+        $zipFile = new ZipFile();
+        $zipFile['file 1'] = 'contents';
+        $zipEntryBeforeWrite = $zipFile->getEntry('file 1');
+        $zipFile->saveAsFile($this->outputFilename);
+        $zipAfterBeforeWrite = $zipFile->getEntry('file 1');
+
+        static::assertSame($zipAfterBeforeWrite, $zipEntryBeforeWrite);
+
+        $zipFile->close();
+    }
+
+    /**
+     * @throws ZipException
+     */
+    public function testMultiSave()
+    {
+        $zipFile = new ZipFile();
+        $zipFile['file 1'] = 'contents';
+        for ($i = 0; $i < 10; $i++) {
+            $zipFile->saveAsFile($this->outputFilename);
+            self::assertCorrectZipArchive($this->outputFilename);
+        }
+        $zipFile->close();
+    }
+
+    /**
+     * @throws ZipEntryNotFoundException
+     * @throws ZipException
+     */
+    public function testNoData()
+    {
+        $this->setExpectedException(ZipException::class, 'No data for zip entry file');
+
+        $entryName = 'file';
+
+        $zipFile = new ZipFile();
+
+        try {
+            $zipFile[$entryName] = '';
+            $zipEntry = $zipFile->getEntry($entryName);
+            $zipEntry->setData(null);
+            $zipFile->getEntryContents($entryName);
+        } finally {
+            $zipFile->close();
+        }
+    }
+
     /**
      * @throws ZipEntryNotFoundException
      * @throws ZipException

+ 118 - 0
tests/ZipInfoTest.php

@@ -0,0 +1,118 @@
+<?php
+
+namespace PhpZip\Tests;
+
+use PhpZip\Constants\ZipCompressionMethod;
+use PhpZip\Constants\ZipEncryptionMethod;
+use PhpZip\Constants\ZipPlatform;
+use PhpZip\Exception\ZipEntryNotFoundException;
+use PhpZip\Exception\ZipException;
+use PhpZip\Model\ZipInfo;
+use PhpZip\ZipFile;
+
+/**
+ * Testing the {@see ZipInfo} class.
+ *
+ * {@see ZipInfo} is {@deprecated}. Use the {@see ZipEntry} class.
+ *
+ * @internal
+ *
+ * @small
+ */
+final class ZipInfoTest extends ZipTestCase
+{
+    public function testZipAllInfo()
+    {
+        $zipFile = new ZipFile();
+        $zipFile['entry'] = 'contents';
+        $zipFile['entry 2'] = 'contents';
+        $zipAllInfo = $zipFile->getAllInfo();
+        $zipFile->close();
+
+        self::assertCount(2, $zipAllInfo);
+        self::assertContainsOnlyInstancesOf(ZipInfo::class, $zipAllInfo);
+    }
+
+    /**
+     * @throws ZipEntryNotFoundException
+     * @throws ZipException
+     */
+    public function testZipEntryInfo()
+    {
+        $zipFile = new ZipFile();
+        $zipFile['entry'] = 'contents';
+        $zipFile['entry 2'] = 'contents';
+        $zipInfo = $zipFile->getEntryInfo('entry');
+        $zipFile->close();
+
+        self::assertInstanceOf(ZipInfo::class, $zipInfo);
+    }
+
+    /**
+     * @throws ZipEntryNotFoundException
+     * @throws ZipException
+     */
+    public function testZipInfoEntryNotFound()
+    {
+        $this->setExpectedException(
+            ZipEntryNotFoundException::class,
+            'Zip Entry "unknown.name" was not found in the archive.'
+        );
+
+        $zipFile = new ZipFile();
+        $zipFile->getEntryInfo('unknown.name');
+    }
+
+    /**
+     * @throws ZipEntryNotFoundException
+     * @throws ZipException
+     */
+    public function testZipInfo()
+    {
+        $zipFile = new ZipFile();
+        $zipFile->openFile(__DIR__ . '/resources/Advanced-v1.0.0.epub');
+        $entryName = 'META-INF/container.xml';
+        $zipEntry = $zipFile->getEntry($entryName);
+        $zipInfo = $zipFile->getEntryInfo($entryName);
+        $zipFile->close();
+
+        self::assertSame($zipInfo->getName(), $zipEntry->getName());
+        self::assertSame($zipInfo->isFolder(), $zipEntry->isDirectory());
+        self::assertSame($zipInfo->getSize(), $zipEntry->getUncompressedSize());
+        self::assertSame($zipInfo->getCompressedSize(), $zipEntry->getCompressedSize());
+        self::assertSame($zipInfo->getMtime(), $zipEntry->getMTime()->getTimestamp());
+        self::assertSame(
+            $zipInfo->getCtime(),
+            $zipEntry->getCTime() !== null ? $zipEntry->getCTime()->getTimestamp() : null
+        );
+        self::assertSame(
+            $zipInfo->getAtime(),
+            $zipEntry->getATime() !== null ? $zipEntry->getATime()->getTimestamp() : null
+        );
+        self::assertNotEmpty($zipInfo->getAttributes());
+        self::assertSame($zipInfo->isEncrypted(), $zipEntry->isEncrypted());
+        self::assertSame($zipInfo->getComment(), $zipEntry->getComment());
+        self::assertSame($zipInfo->getCrc(), $zipEntry->getCrc());
+        self::assertSame(
+            $zipInfo->getMethod(),
+            ZipCompressionMethod::getCompressionMethodName($zipEntry->getCompressionMethod())
+        );
+        self::assertSame(
+            $zipInfo->getMethodName(),
+            ZipCompressionMethod::getCompressionMethodName($zipEntry->getCompressionMethod())
+        );
+        self::assertSame(
+            $zipInfo->getEncryptionMethodName(),
+            ZipEncryptionMethod::getEncryptionMethodName($zipEntry->getEncryptionMethod())
+        );
+        self::assertSame($zipInfo->getPlatform(), ZipPlatform::getPlatformName($zipEntry->getExtractedOS()));
+        self::assertSame(ZipInfo::getPlatformName($zipEntry), ZipPlatform::getPlatformName($zipEntry->getExtractedOS()));
+        self::assertSame($zipInfo->getVersion(), $zipEntry->getExtractVersion());
+        self::assertNull($zipInfo->getEncryptionMethod());
+        self::assertSame($zipInfo->getCompressionLevel(), $zipEntry->getCompressionLevel());
+        self::assertSame($zipInfo->getCompressionMethod(), $zipEntry->getCompressionMethod());
+        self::assertNotEmpty($zipInfo->toArray());
+
+        self::assertSame((string) $zipInfo, 'PhpZip\Model\ZipInfo {Name="META-INF/container.xml", Size="249 bytes", Compressed size="169 bytes", Modified time="2019-04-08T14:59:08+00:00", Comment="", Method name="Deflated", Attributes="------", Platform="MS-DOS", Version=20}');
+    }
+}

BIN
tests/resources/Advanced-v1.0.0.epub


Bu fark içinde çok fazla dosya değişikliği olduğu için bazı dosyalar gösterilmiyor