wapplay 4 лет назад
Родитель
Сommit
501b52f6fc

+ 23 - 14
.php_cs

@@ -1,7 +1,7 @@
 <?php
 
 /**
- * PHP Code Style Fixer (config created for version 2.16.1 (Yellow Bird)).
+ * PHP Code Style Fixer (config created for version 2.16.4 (Yellow Bird)).
  *
  * Use one of the following console commands to just see the
  * changes that will be made.
@@ -119,7 +119,7 @@ $rules = [
      *
      * Risky!
      * Risky as new docblocks might mean more, e.g. a Doctrine entity
-     * might have a new column in database
+     * might have a new column in database.
      */
     'comment_to_phpdoc' => [
         'ignored_tags' => [
@@ -238,6 +238,7 @@ $rules = [
             'iconv',
             'mime_content_type',
             'rename',
+            'rmdir',
             'unlink',
         ],
     ],
@@ -283,7 +284,7 @@ $rules = [
      * - Explicit syntax allows word concatenation inside strings, e.g.
      * `"${var}IsAVar"`, implicit doesn't
      * - Explicit syntax is easier to detect for IDE/editors and
-     * therefore has colors/hightlight with higher contrast, which is
+     * therefore has colors/highlight with higher contrast, which is
      * easier to read
      * Backtick operator is skipped because it is harder to handle; you
      * can use `backtick_to_shell_exec` fixer to normalize backticks to
@@ -327,7 +328,7 @@ $rules = [
      * want to override a method, use the Template method pattern.
      *
      * Risky!
-     * Risky when overriding `public` methods of `abstract` classes
+     * Risky when overriding `public` methods of `abstract` classes.
      */
     'final_public_method_for_abstract_class' => false,
 
@@ -725,8 +726,8 @@ $rules = [
     'no_superfluous_elseif' => true,
 
     /*
-     * Removes `@param` and `@return` tags that don't provide any useful
-     * information.
+     * Removes `@param`, `@return` and `@var` tags that don't provide
+     * any useful information.
      */
     'no_superfluous_phpdoc_tags' => false,
 
@@ -751,7 +752,13 @@ $rules = [
      */
     'no_unneeded_curly_braces' => true,
 
-    // A final class must not have final methods.
+    /*
+     * A `final` class must not have `final` methods and `private`
+     * methods must not be `final`.
+     *
+     * Risky!
+     * Risky when child class overrides a `private` method.
+     */
     'no_unneeded_final_method' => true,
 
     /*
@@ -1140,7 +1147,7 @@ $rules = [
      * adjusts accordingly the function signature. Requires PHP >= 7.0.
      *
      * Risky!
-     * [1] This rule is EXPERIMENTAL and is not covered with backward
+     * This rule is EXPERIMENTAL and [1] is not covered with backward
      * compatibility promise. [2] `@param` annotation is mandatory for
      * the fixer to make changes, signatures of methods without it (no
      * docblock, inheritdocs) will not be fixed. [3] Manual actions are
@@ -1153,7 +1160,7 @@ $rules = [
      * adjusts accordingly the function signature. Requires PHP >= 7.0.
      *
      * Risky!
-     * [1] This rule is EXPERIMENTAL and is not covered with backward
+     * This rule is EXPERIMENTAL and [1] is not covered with backward
      * compatibility promise. [2] `@return` annotation is mandatory for
      * the fixer to make changes, signatures of methods without it (no
      * docblock, inheritdocs) will not be fixed. [3] Manual actions are
@@ -1190,8 +1197,8 @@ $rules = [
     'phpdoc_var_annotation_correct_order' => true,
 
     /*
-     * `@var` and `@type` annotations should not contain the variable
-     * name.
+     * `@var` and `@type` annotations of classy properties should not
+     * contain the name.
      */
     'phpdoc_var_without_name' => false,
 
@@ -1366,7 +1373,7 @@ $rules = [
      * `static`.
      *
      * Risky!
-     * Risky when using "->bindTo" on lambdas without referencing to
+     * Risky when using `->bindTo` on lambdas without referencing to
      * `$this`.
      */
     'static_lambda' => true,
@@ -1464,12 +1471,14 @@ $rules = [
 
 if (\PHP_SAPI === 'cli' && !class_exists(\PhpCsFixer\Config::class)) {
     $binFixer = __DIR__ . '/vendor/bin/php-cs-fixer';
+
     if (!is_file($binFixer)) {
         $binFixer = 'php-cs-fixer';
     }
-    $dryRun = !\in_array('--force', $_SERVER['argv'], true);
+    $dryRun = !in_array('--force', $_SERVER['argv'], true);
+
+    $command = escapeshellarg($binFixer) . ' fix --config ' . escapeshellarg(__FILE__) . ' --diff-format udiff --ansi -vv';
 
-    $command = escapeshellarg($binFixer) . ' fix --config ' . escapeshellarg(__FILE__) . ' --diff-format udiff --ansi';
     if ($dryRun) {
         $command .= ' --dry-run';
     }

+ 21 - 0
LICENSE

@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2016-2020 Ne-Lexa
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.

+ 3 - 1
src/IO/ZipReader.php

@@ -354,7 +354,9 @@ class ZipReader
         fseek($this->inStream, $cdOffset);
 
         if (!($cdStream = fopen('php://temp', 'w+b'))) {
-            throw new ZipException('Temp resource can not open from write');
+            // @codeCoverageIgnoreStart
+            throw new ZipException('A temporary resource cannot be opened for writing.');
+            // @codeCoverageIgnoreEnd
         }
         stream_copy_to_stream($this->inStream, $cdStream, $endCD->getCdSize());
         rewind($cdStream);

+ 4 - 2
src/Model/Data/ZipNewData.php

@@ -39,7 +39,9 @@ class ZipNewData implements ZipData
             $zipEntry->setUncompressedSize(\strlen($data));
 
             if (!($handle = fopen('php://temp', 'w+b'))) {
-                throw new \RuntimeException('Temp resource can not open from write.');
+                // @codeCoverageIgnoreStart
+                throw new \RuntimeException('A temporary resource cannot be opened for writing.');
+                // @codeCoverageIgnoreEnd
             }
             fwrite($handle, $data);
             rewind($handle);
@@ -61,7 +63,7 @@ class ZipNewData implements ZipData
     public function getDataAsStream()
     {
         if (!\is_resource($this->stream)) {
-            throw new \LogicException(sprintf('Resource was closed (entry=%s).', $this->zipEntry->getName()));
+            throw new \LogicException(sprintf('Resource has been closed (entry=%s).', $this->zipEntry->getName()));
         }
 
         return $this->stream;

+ 4 - 4
src/Util/FilesUtil.php

@@ -48,7 +48,7 @@ final class FilesUtil
             $function = ($fileInfo->isDir() ? 'rmdir' : 'unlink');
             $function($fileInfo->getPathname());
         }
-        rmdir($dir);
+        @rmdir($dir);
     }
 
     /**
@@ -198,10 +198,10 @@ final class FilesUtil
             return $files;
         }
 
-        foreach (glob(\dirname($globPattern) . '/*', \GLOB_ONLYDIR | \GLOB_NOSORT) as $dir) {
+        foreach (glob(\dirname($globPattern) . \DIRECTORY_SEPARATOR . '*', \GLOB_ONLYDIR | \GLOB_NOSORT) as $dir) {
             // Unpacking the argument via ... is supported starting from php 5.6 only
             /** @noinspection SlowArrayOperationsInLoopInspection */
-            $files = array_merge($files, self::globFileSearch($dir . '/' . basename($globPattern), $flags, $recursive));
+            $files = array_merge($files, self::globFileSearch($dir . \DIRECTORY_SEPARATOR . basename($globPattern), $flags, $recursive));
         }
 
         return $files;
@@ -273,7 +273,7 @@ final class FilesUtil
     public static function normalizeZipPath($path)
     {
         return implode(
-            '/',
+            \DIRECTORY_SEPARATOR,
             array_filter(
                 explode('/', (string) $path),
                 static function ($part) {

+ 65 - 64
src/ZipFile.php

@@ -1,9 +1,5 @@
 <?php
 
-/** @noinspection AdditionOperationOnArraysInspection */
-
-/** @noinspection PhpUsageOfSilenceOperatorInspection */
-
 namespace PhpZip;
 
 use PhpZip\Constants\UnixStat;
@@ -143,7 +139,7 @@ class ZipFile implements ZipFileInterface
 
         if (!($handle = fopen('php://temp', 'r+b'))) {
             // @codeCoverageIgnoreStart
-            throw new ZipException("Can't open temp stream.");
+            throw new ZipException('A temporary resource cannot be opened for writing.');
             // @codeCoverageIgnoreEnd
         }
         fwrite($handle, $data);
@@ -258,8 +254,8 @@ class ZipFile implements ZipFileInterface
      *
      * @param string $entryName
      *
-     * @throws ZipException
      * @throws ZipEntryNotFoundException
+     * @throws ZipException
      *
      * @return string
      */
@@ -274,8 +270,8 @@ class ZipFile implements ZipFileInterface
      * @param string      $entryName
      * @param string|null $comment
      *
-     * @throws ZipEntryNotFoundException
      * @throws ZipException
+     * @throws ZipEntryNotFoundException
      *
      * @return ZipFile
      */
@@ -291,8 +287,8 @@ class ZipFile implements ZipFileInterface
      *
      * @param string $entryName
      *
-     * @throws ZipEntryNotFoundException
      * @throws ZipException
+     * @throws ZipEntryNotFoundException
      *
      * @return string
      */
@@ -310,8 +306,8 @@ class ZipFile implements ZipFileInterface
     /**
      * @param string $entryName
      *
-     * @throws ZipEntryNotFoundException
      * @throws ZipException
+     * @throws ZipEntryNotFoundException
      *
      * @return resource
      */
@@ -328,8 +324,8 @@ class ZipFile implements ZipFileInterface
      *
      * @param string|ZipEntry $entryName
      *
-     * @throws ZipException
      * @throws ZipEntryNotFoundException
+     * @throws ZipException
      *
      * @return ZipInfo
      */
@@ -411,6 +407,7 @@ class ZipFile implements ZipFileInterface
         $defaultOptions = [
             ZipOptions::EXTRACT_SYMLINKS => false,
         ];
+        /** @noinspection AdditionOperationOnArraysInspection */
         $options += $defaultOptions;
 
         $zipEntries = $this->zipContainer->getEntries();
@@ -443,9 +440,6 @@ class ZipFile implements ZipFileInterface
             $entryName = FilesUtil::normalizeZipPath($entryName);
             $file = $destDir . \DIRECTORY_SEPARATOR . $entryName;
 
-            if (\DIRECTORY_SEPARATOR === '\\') {
-                $file = str_replace('/', '\\', $file);
-            }
             $extractedEntries[$file] = $entry;
             $modifyTimestamp = $entry->getMTime()->getTimestamp();
             $atime = $entry->getATime();
@@ -568,19 +562,12 @@ class ZipFile implements ZipFileInterface
      */
     public function addFromString($entryName, $contents, $compressionMethod = null)
     {
-        if ($entryName === null) {
-            throw new InvalidArgumentException('Entry name is null');
-        }
+        $entryName = $this->normalizeEntryName($entryName);
 
         if ($contents === null) {
             throw new InvalidArgumentException('Contents is null');
         }
 
-        $entryName = ltrim((string) $entryName, '\\/');
-
-        if ($entryName === '') {
-            throw new InvalidArgumentException('Empty entry name');
-        }
         $contents = (string) $contents;
         $length = \strlen($contents);
 
@@ -609,6 +596,30 @@ class ZipFile implements ZipFileInterface
         return $this;
     }
 
+    /**
+     * @param string $entryName
+     *
+     * @return string
+     */
+    protected function normalizeEntryName($entryName)
+    {
+        if ($entryName === null) {
+            throw new InvalidArgumentException('Entry name is null');
+        }
+
+        $entryName = ltrim((string) $entryName, '\\/');
+
+        if (\DIRECTORY_SEPARATOR === '\\') {
+            $entryName = str_replace('\\', '/', $entryName);
+        }
+
+        if ($entryName === '') {
+            throw new InvalidArgumentException('Empty entry name');
+        }
+
+        return $entryName;
+    }
+
     /**
      * @param Finder $finder
      * @param array  $options
@@ -624,6 +635,7 @@ class ZipFile implements ZipFileInterface
             ZipOptions::COMPRESSION_METHOD => null,
             ZipOptions::MODIFIED_TIME => null,
         ];
+        /** @noinspection AdditionOperationOnArraysInspection */
         $options += $defaultOptions;
 
         if ($options[ZipOptions::STORE_ONLY_FILES]) {
@@ -660,6 +672,7 @@ class ZipFile implements ZipFileInterface
             ZipOptions::COMPRESSION_METHOD => null,
             ZipOptions::MODIFIED_TIME => null,
         ];
+        /** @noinspection AdditionOperationOnArraysInspection */
         $options += $defaultOptions;
 
         if (!$file->isReadable()) {
@@ -674,12 +687,7 @@ class ZipFile implements ZipFileInterface
             }
         }
 
-        $entryName = ltrim((string) $entryName, '\\/');
-
-        if ($entryName === '') {
-            throw new InvalidArgumentException('Empty entry name');
-        }
-
+        $entryName = $this->normalizeEntryName($entryName);
         $entryName = $file->isDir() ? rtrim($entryName, '/\\') . '/' : $entryName;
 
         $zipEntry = new ZipEntry($entryName);
@@ -814,17 +822,9 @@ class ZipFile implements ZipFileInterface
             throw new InvalidArgumentException('Stream is not resource');
         }
 
-        if ($entryName === null) {
-            throw new InvalidArgumentException('Entry name is null');
-        }
-        $entryName = ltrim((string) $entryName, '\\/');
-
-        if ($entryName === '') {
-            throw new InvalidArgumentException('Empty entry name');
-        }
-        $fstat = fstat($stream);
-
+        $entryName = $this->normalizeEntryName($entryName);
         $zipEntry = new ZipEntry($entryName);
+        $fstat = fstat($stream);
 
         if ($fstat !== false) {
             $unixMode = $fstat['mode'];
@@ -875,14 +875,7 @@ class ZipFile implements ZipFileInterface
      */
     public function addEmptyDir($dirName)
     {
-        if ($dirName === null) {
-            throw new InvalidArgumentException('Dir name is null');
-        }
-        $dirName = ltrim((string) $dirName, '\\/');
-
-        if ($dirName === '') {
-            throw new InvalidArgumentException('Empty dir name');
-        }
+        $dirName = $this->normalizeEntryName($dirName);
         $dirName = rtrim($dirName, '\\/') . '/';
 
         $zipEntry = new ZipEntry($dirName);
@@ -1077,6 +1070,7 @@ class ZipFile implements ZipFileInterface
      * @throws ZipException
      *
      * @return ZipFile
+     *
      * @sse https://en.wikipedia.org/wiki/Glob_(programming) Glob pattern syntax
      */
     private function addGlob(
@@ -1604,19 +1598,39 @@ class ZipFile implements ZipFileInterface
     {
         $filename = (string) $filename;
 
-        $tempFilename = $filename . '.temp' . uniqid('', true);
+        $tempFilename = $filename . '.temp' . uniqid('', false);
 
         if (!($handle = @fopen($tempFilename, 'w+b'))) {
-            throw new InvalidArgumentException('File ' . $tempFilename . ' can not open from write.');
+            throw new InvalidArgumentException(sprintf('Cannot open "%s" for writing.', $tempFilename));
         }
         $this->saveAsStream($handle);
 
+        $reopen = false;
+
+        if ($this->reader !== null) {
+            $meta = $this->reader->getStreamMetaData();
+
+            if ($meta['wrapper_type'] === 'plainfile' && isset($meta['uri'])) {
+                $readFilePath = realpath($meta['uri']);
+                $writeFilePath = realpath($filename);
+
+                if ($readFilePath !== false && $writeFilePath !== false && $readFilePath === $writeFilePath) {
+                    $this->reader->close();
+                    $reopen = true;
+                }
+            }
+        }
+
         if (!@rename($tempFilename, $filename)) {
             if (is_file($tempFilename)) {
                 unlink($tempFilename);
             }
 
-            throw new ZipException('Can not move ' . $tempFilename . ' to ' . $filename);
+            throw new ZipException(sprintf('Cannot move %s to %s', $tempFilename, $filename));
+        }
+
+        if ($reopen) {
+            return $this->openFile($filename);
         }
 
         return $this;
@@ -1822,24 +1836,11 @@ class ZipFile implements ZipFileInterface
 
         $meta = $this->reader->getStreamMetaData();
 
-        if ($meta['wrapper_type'] === 'plainfile' && isset($meta['uri'])) {
-            $this->saveAsFile($meta['uri']);
-            $this->close();
-
-            if (!($handle = @fopen($meta['uri'], 'rb'))) {
-                throw new ZipException("File {$meta['uri']} can't open.");
-            }
-        } else {
-            $handle = @fopen('php://temp', 'r+b');
-
-            if (!$handle) {
-                throw new ZipException('php://temp cannot open for write.');
-            }
-            $this->writeZipToStream($handle);
-            $this->close();
+        if ($meta['wrapper_type'] !== 'plainfile' || !isset($meta['uri'])) {
+            throw new ZipException('Overwrite is only supported for open local files.');
         }
 
-        return $this->openFromStream($handle);
+        return $this->saveAsFile($meta['uri']);
     }
 
     /**

+ 1 - 1
tests/Internal/DummyFileSystemStream.php

@@ -40,7 +40,7 @@ class DummyFileSystemStream
     public function stream_open($path, $mode, $options, &$opened_path)
     {
         $parsedUrl = parse_url($path);
-        $uri = str_replace('//', '/', $parsedUrl['path']);
+        $uri = substr($parsedUrl['path'], 1);
         $this->fp = @fopen($uri, $mode);
 
         return $this->fp !== false;

+ 1 - 1
tests/SlowTests/Zip64Test.php

@@ -76,7 +76,7 @@ class Zip64Test extends ZipTestCase
             self::assertCorrectZipArchive($this->outputFilename);
 
             if (!is_dir($this->outputDirname)) {
-                mkdir($this->outputDirname, 0755, true);
+                static::assertTrue(mkdir($this->outputDirname, 0755, true));
             }
 
             $zipFile->openFile($this->outputFilename);

+ 5 - 15
tests/SymlinkTest.php

@@ -12,22 +12,8 @@ use Symfony\Component\Finder\Finder;
  *
  * @small
  */
-final class SymlinkTest extends ZipFileTest
+final class SymlinkTest extends ZipTestCase
 {
-    /**
-     * 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
      *
@@ -37,6 +23,10 @@ final class SymlinkTest extends ZipFileTest
      */
     public function testSymlink($allowSymlink)
     {
+        if (self::skipTestForWindows()) {
+            return;
+        }
+
         if (!is_dir($this->outputDirname)) {
             self::assertTrue(mkdir($this->outputDirname, 0755, true));
         }

+ 1 - 1
tests/ZipFileSetTestCase.php

@@ -75,7 +75,7 @@ abstract class ZipFileSetTestCase extends ZipTestCase
             $zipEntryName = $localPath . $file;
 
             if (isset($actualResultFiles[$file])) {
-                static::assertTrue(isset($zipFile[$zipEntryName]));
+                static::assertTrue(isset($zipFile[$zipEntryName]), 'Not found entry name ' . $zipEntryName);
                 static::assertSame(
                     $zipFile[$zipEntryName],
                     $content,

+ 110 - 10
tests/ZipFileTest.php

@@ -34,7 +34,7 @@ class ZipFileTest extends ZipTestCase
         $this->setExpectedException(ZipException::class, 'does not exist');
 
         $zipFile = new ZipFile();
-        $zipFile->openFile(uniqid('', true));
+        $zipFile->openFile(uniqid('', false));
     }
 
     /**
@@ -42,12 +42,16 @@ class ZipFileTest extends ZipTestCase
      */
     public function testOpenFileCantOpen()
     {
-        $this->setExpectedException(ZipException::class, 'can\'t open');
+        if (static::skipTestForWindows()) {
+            return;
+        }
 
         if (static::skipTestForRootUser()) {
             return;
         }
 
+        $this->setExpectedException(ZipException::class, 'can\'t open');
+
         static::assertNotFalse(file_put_contents($this->outputFilename, 'content'));
         static::assertTrue(chmod($this->outputFilename, 0222));
 
@@ -1031,7 +1035,12 @@ class ZipFileTest extends ZipTestCase
         $zipFile->extractTo($this->outputDirname, null, [], $extractedEntries);
 
         foreach ($entries as $entryName => $contents) {
-            $fullExtractedFilename = $this->outputDirname . \DIRECTORY_SEPARATOR . $entryName;
+            $name = $entryName;
+
+            if (\DIRECTORY_SEPARATOR === '\\') {
+                $name = str_replace('/', '\\', $name);
+            }
+            $fullExtractedFilename = $this->outputDirname . \DIRECTORY_SEPARATOR . $name;
 
             static::assertTrue(
                 isset($extractedEntries[$fullExtractedFilename]),
@@ -1393,14 +1402,18 @@ class ZipFileTest extends ZipTestCase
     /**
      * @throws ZipException
      */
-    public function testAddFileCantOpen()
+    public function testAddFileCannotOpen()
     {
-        $this->setExpectedException(InvalidArgumentException::class, 'is not readable');
+        if (static::skipTestForWindows()) {
+            return;
+        }
 
         if (static::skipTestForRootUser()) {
             return;
         }
 
+        $this->setExpectedException(InvalidArgumentException::class, 'is not readable');
+
         static::assertNotFalse(file_put_contents($this->outputFilename, ''));
         static::assertTrue(chmod($this->outputFilename, 0244));
 
@@ -1438,7 +1451,7 @@ class ZipFileTest extends ZipTestCase
         $this->setExpectedException(InvalidArgumentException::class, 'does not exist');
 
         $zipFile = new ZipFile();
-        $zipFile->addDir(uniqid('', true));
+        $zipFile->addDir(uniqid('', false));
     }
 
     /**
@@ -1471,7 +1484,7 @@ class ZipFileTest extends ZipTestCase
         $this->setExpectedException(InvalidArgumentException::class, 'does not exist');
 
         $zipFile = new ZipFile();
-        $zipFile->addDirRecursive(uniqid('', true));
+        $zipFile->addDirRecursive(uniqid('', false));
     }
 
     /**
@@ -1711,7 +1724,9 @@ class ZipFileTest extends ZipTestCase
      */
     public function testSaveAsFileNotWritable()
     {
-        $this->setExpectedException(InvalidArgumentException::class, 'can not open from write');
+        if (static::skipTestForWindows()) {
+            return;
+        }
 
         if (static::skipTestForRootUser()) {
             return;
@@ -1722,6 +1737,8 @@ class ZipFileTest extends ZipTestCase
 
         $this->outputFilename = $this->outputDirname . \DIRECTORY_SEPARATOR . basename($this->outputFilename);
 
+        $this->setExpectedExceptionRegExp(InvalidArgumentException::class, '~Cannot open ".*?" for writing.~');
+
         $zipFile = new ZipFile();
         $zipFile->saveAsFile($this->outputFilename);
     }
@@ -1877,7 +1894,7 @@ class ZipFileTest extends ZipTestCase
      */
     public function testAddEmptyDirNullName()
     {
-        $this->setExpectedException(InvalidArgumentException::class, 'Dir name is null');
+        $this->setExpectedException(InvalidArgumentException::class, 'Entry name is null');
 
         $zipFile = new ZipFile();
         $zipFile->addEmptyDir(null);
@@ -1888,7 +1905,7 @@ class ZipFileTest extends ZipTestCase
      */
     public function testAddEmptyDirEmptyName()
     {
-        $this->setExpectedException(InvalidArgumentException::class, 'Empty dir name');
+        $this->setExpectedException(InvalidArgumentException::class, 'Empty entry name');
 
         $zipFile = new ZipFile();
         $zipFile->addEmptyDir('');
@@ -1943,6 +1960,8 @@ class ZipFileTest extends ZipTestCase
      */
     public function testRewriteString()
     {
+        $this->setExpectedException(ZipException::class, 'Overwrite is only supported for open local files');
+
         $zipFile = new ZipFile();
         $zipFile['file'] = 'content';
         $zipFile['file2'] = 'content2';
@@ -1973,6 +1992,87 @@ class ZipFileTest extends ZipTestCase
         $zipFile->rewrite();
     }
 
+    /**
+     * Checks the ability to overwrite an open zip file with a relative path.
+     *
+     * @throws ZipException
+     */
+    public function testRewriteRelativeFile()
+    {
+        $zipFile = new ZipFile();
+        $zipFile['entry.txt'] = 'test';
+        $zipFile->saveAsFile($this->outputFilename);
+        $zipFile->close();
+
+        $outputDirname = \dirname($this->outputFilename);
+        static::assertTrue(chdir($outputDirname));
+
+        $relativeFilename = basename($this->outputFilename);
+
+        $zipFile->openFile($this->outputFilename);
+        $zipFile['entry2.txt'] = 'test';
+        $zipFile->saveAsFile($relativeFilename);
+        $zipFile->close();
+
+        self::assertCorrectZipArchive($this->outputFilename);
+    }
+
+    /**
+     * Checks the ability to overwrite an open zip file with a relative path.
+     *
+     * @throws ZipException
+     */
+    public function testRewriteDifferentWinDirectorySeparator()
+    {
+        if (\DIRECTORY_SEPARATOR !== '\\') {
+            static::markTestSkipped('Windows test only');
+
+            return;
+        }
+
+        $zipFile = new ZipFile();
+        $zipFile['entry.txt'] = 'test';
+        $zipFile->saveAsFile($this->outputFilename);
+        $zipFile->close();
+
+        $alternativeOutputFilename = str_replace('\\', '/', $this->outputFilename);
+        self::assertCorrectZipArchive($alternativeOutputFilename);
+
+        $zipFile->openFile($this->outputFilename);
+        $zipFile['entry2.txt'] = 'test';
+        $zipFile->saveAsFile($alternativeOutputFilename);
+        $zipFile->close();
+
+        self::assertCorrectZipArchive($alternativeOutputFilename);
+
+        $zipFile->openFile($this->outputFilename);
+        static::assertCount(2, $zipFile);
+        $zipFile->close();
+    }
+
+    /**
+     * @throws ZipException
+     */
+    public function testRewriteRelativeFile2()
+    {
+        $this->outputFilename = basename($this->outputFilename);
+
+        $zipFile = new ZipFile();
+        $zipFile['entry.txt'] = 'test';
+        $zipFile->saveAsFile($this->outputFilename);
+        $zipFile->close();
+
+        $absoluteOutputFilename = getcwd() . \DIRECTORY_SEPARATOR . $this->outputFilename;
+        self::assertCorrectZipArchive($absoluteOutputFilename);
+
+        $zipFile->openFile($this->outputFilename);
+        $zipFile['entry2.txt'] = 'test';
+        $zipFile->saveAsFile($absoluteOutputFilename);
+        $zipFile->close();
+
+        self::assertCorrectZipArchive($absoluteOutputFilename);
+    }
+
     /**
      * @throws ZipException
      */

+ 3 - 2
tests/ZipPasswordTest.php

@@ -77,7 +77,8 @@ class ZipPasswordTest extends ZipFileSetTestCase
         $zipFile->saveAsFile($this->outputFilename);
         $zipFile->close();
 
-        static::assertCorrectZipArchive($this->outputFilename, $password);
+        /** @see https://sourceforge.net/p/p7zip/discussion/383044/thread/c859a2f0/ WinZip 99-character limit */
+        static::assertCorrectZipArchive($this->outputFilename, substr($password, 0, 99));
 
         // check from WinZip AES encryption
         $zipFile->openFile($this->outputFilename);
@@ -137,7 +138,7 @@ class ZipPasswordTest extends ZipFileSetTestCase
             );
         }
 
-        $password = base64_encode(random_bytes(50));
+        $password = md5(random_bytes(50));
 
         $zip = new ZipFile();
         $zip->addDirRecursive($this->outputDirname);

+ 2 - 2
tests/ZipStreamOpenTest.php

@@ -29,8 +29,8 @@ class ZipStreamOpenTest extends TestCase
      */
     public function testOpenStream($resource, $exceptionClass = null, $exceptionMessage = null)
     {
-        if ($resource === null) {
-            static::markTestSkipped('skip null resource');
+        if ($resource === null || $resource === false) {
+            static::markTestSkipped('skip resource');
 
             return;
         }

+ 100 - 51
tests/ZipTestCase.php

@@ -24,14 +24,14 @@ abstract class ZipTestCase extends TestCase
      */
     protected function setUp()
     {
-        $id = uniqid('phpzip', true);
-        $tempDir = sys_get_temp_dir() . '/phpunit-phpzip';
+        $id = uniqid('phpzip', false);
+        $tempDir = sys_get_temp_dir() . \DIRECTORY_SEPARATOR . 'phpunit-phpzip';
 
         if (!is_dir($tempDir) && !mkdir($tempDir, 0755, true) && !is_dir($tempDir)) {
-            throw new \RuntimeException('Dir ' . $tempDir . " can't created");
+            throw new \RuntimeException(sprintf('Directory "%s" was not created', $tempDir));
         }
-        $this->outputFilename = $tempDir . '/' . $id . '.zip';
-        $this->outputDirname = $tempDir . '/' . $id;
+        $this->outputFilename = $tempDir . \DIRECTORY_SEPARATOR . $id . '.zip';
+        $this->outputDirname = $tempDir . \DIRECTORY_SEPARATOR . $id;
     }
 
     /**
@@ -58,64 +58,91 @@ abstract class ZipTestCase extends TestCase
      */
     public static function assertCorrectZipArchive($filename, $password = null)
     {
-        if (self::existsProgram('unzip')) {
-            $command = 'unzip';
+        if (self::existsProgram('7z')) {
+            self::assertCorrectZipArchiveFrom7z($filename, $password);
+        } elseif (self::existsProgram('unzip')) {
+            self::assertCorrectZipArchiveFromUnzip($filename, $password);
+        } else {
+            fwrite(\STDERR, 'Skipped testing the zip archive for errors using third-party utilities.' . \PHP_EOL);
+            fwrite(\STDERR, 'To fix this, install 7-zip or unzip.' . \PHP_EOL);
+            fwrite(\STDERR, \PHP_EOL);
+            fwrite(\STDERR, 'Install on Ubuntu: sudo apt-get install p7zip-full unzip' . \PHP_EOL);
+            fwrite(\STDERR, \PHP_EOL);
+            fwrite(\STDERR, 'Install on Windows:' . \PHP_EOL);
+            fwrite(\STDERR, ' * 7-zip - https://www.7-zip.org/download.html' . \PHP_EOL);
+            fwrite(\STDERR, ' * unzip - http://gnuwin32.sourceforge.net/packages/unzip.htm' . \PHP_EOL);
+            fwrite(\STDERR, \PHP_EOL);
+        }
+    }
 
-            if ($password !== null) {
-                $command .= ' -P ' . escapeshellarg($password);
-            }
-            $command .= ' -t ' . escapeshellarg($filename);
-            $command .= ' 2>&1';
-            exec($command, $output, $returnCode);
+    /**
+     * @param string      $filename
+     * @param string|null $password
+     */
+    private static function assertCorrectZipArchiveFrom7z($filename, $password = null)
+    {
+        $command = '7z t';
 
-            $output = implode(\PHP_EOL, $output);
+        if ($password !== null) {
+            $command .= ' -p' . escapeshellarg($password);
+        }
+        $command .= ' ' . escapeshellarg($filename) . ' 2>&1';
 
-            if ($password !== null && $returnCode === 81) {
-                if (self::existsProgram('7z')) {
-                    /**
-                     * WinZip 99-character limit.
-                     *
-                     * @see https://sourceforge.net/p/p7zip/discussion/383044/thread/c859a2f0/
-                     */
-                    $password = substr($password, 0, 99);
-
-                    $command = '7z t -p' . escapeshellarg($password) . ' ' . escapeshellarg($filename);
-                    exec($command, $output, $returnCode);
-                    /**
-                     * @var array $output
-                     */
-                    $output = implode(\PHP_EOL, $output);
-
-                    static::assertSame($returnCode, 0);
-                    static::assertNotContains(' Errors', $output);
-                    static::assertContains(' Ok', $output);
-                } else {
-                    fwrite(\STDERR, 'Program unzip cannot support this function.' . \PHP_EOL);
-                    fwrite(\STDERR, 'Please install 7z. For Ubuntu-like: sudo apt-get install p7zip-full' . \PHP_EOL);
-                }
-            } else {
-                static::assertSame($returnCode, 0, $output);
-                static::assertNotContains('incorrect password', $output);
-                static::assertContains(' OK', $output);
-                static::assertContains('No errors', $output);
-            }
+        exec($command, $output, $returnCode);
+        $output = implode(\PHP_EOL, $output);
+
+        static::assertSame($returnCode, 0);
+        static::assertNotContains(' Errors', $output);
+        static::assertContains(' Ok', $output);
+    }
+
+    /**
+     * @param string      $filename
+     * @param string|null $password
+     */
+    private static function assertCorrectZipArchiveFromUnzip($filename, $password = null)
+    {
+        $command = 'unzip';
+
+        if ($password !== null) {
+            $command .= ' -P ' . escapeshellarg($password);
         }
+        $command .= ' -t ' . escapeshellarg($filename) . ' 2>&1';
+
+        exec($command, $output, $returnCode);
+        $output = implode(\PHP_EOL, $output);
+
+        if ($password !== null && $returnCode === 81) {
+            fwrite(\STDERR, 'Program unzip cannot support this function.' . \PHP_EOL);
+            fwrite(\STDERR, 'You have to install 7-zip to complete this test.' . \PHP_EOL);
+            fwrite(\STDERR, 'Install 7-Zip on Ubuntu: sudo apt-get install p7zip-full' . \PHP_EOL);
+            fwrite(\STDERR, 'Install 7-Zip on Windows: https://www.7-zip.org/download.html' . \PHP_EOL);
+
+            return;
+        }
+
+        static::assertSame($returnCode, 0, $output);
+        static::assertNotContains('incorrect password', $output);
+        static::assertContains(' OK', $output);
+        static::assertContains('No errors', $output);
     }
 
     /**
      * @param string $program
+     * @param array  $successCodes
      *
      * @return bool
      */
-    protected static function existsProgram($program)
+    protected static function existsProgram($program, array $successCodes = [0])
     {
-        if (\DIRECTORY_SEPARATOR !== '\\') {
-            exec('which ' . escapeshellarg($program), $output, $returnCode);
+        $command = \DIRECTORY_SEPARATOR === '\\' ?
+            escapeshellarg($program) :
+            'which ' . escapeshellarg($program);
+        $command .= ' 2>&1';
 
-            return $returnCode === 0;
-        }
-        // false for Windows
-        return false;
+        exec($command, $output, $returnCode);
+
+        return \in_array($returnCode, $successCodes, true);
     }
 
     /**
@@ -144,7 +171,7 @@ abstract class ZipTestCase extends TestCase
      */
     public static function assertVerifyZipAlign($filename, $showErrors = false)
     {
-        if (self::existsProgram('zipalign')) {
+        if (self::existsProgram('zipalign', [0, 2])) {
             exec('zipalign -c -v 4 ' . escapeshellarg($filename), $output, $returnCode);
 
             if ($showErrors && $returnCode !== 0) {
@@ -155,6 +182,14 @@ abstract class ZipTestCase extends TestCase
         }
 
         fwrite(\STDERR, "Cannot find the program 'zipalign' for the test" . \PHP_EOL);
+        fwrite(\STDERR, 'To fix this, install zipalign.' . \PHP_EOL);
+        fwrite(\STDERR, \PHP_EOL);
+        fwrite(\STDERR, 'Install on Ubuntu: sudo apt-get install zipalign' . \PHP_EOL);
+        fwrite(\STDERR, \PHP_EOL);
+        fwrite(\STDERR, 'Install on Windows:' . \PHP_EOL);
+        fwrite(\STDERR, ' 1. Install Android Studio' . \PHP_EOL);
+        fwrite(\STDERR, ' 2. Install Android Sdk' . \PHP_EOL);
+        fwrite(\STDERR, ' 3. Add zipalign path to \$Path' . \PHP_EOL);
 
         return null;
     }
@@ -173,4 +208,18 @@ abstract class ZipTestCase extends TestCase
 
         return false;
     }
+
+    /**
+     * @return bool
+     */
+    public static function skipTestForWindows()
+    {
+        if (\DIRECTORY_SEPARATOR === '\\') {
+            static::markTestSkipped('Skip on Windows');
+
+            return true;
+        }
+
+        return false;
+    }
 }