diff --git a/src/Model/Data/ZipNewData.php b/src/Model/Data/ZipNewData.php index 4b1dd0e..68f76f6 100644 --- a/src/Model/Data/ZipNewData.php +++ b/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 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); } diff --git a/src/Model/ImmutableZipContainer.php b/src/Model/ImmutableZipContainer.php index 72e0d33..69722a0 100644 --- a/src/Model/ImmutableZipContainer.php +++ b/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; + } + } } diff --git a/src/Model/ZipContainer.php b/src/Model/ZipContainer.php index f3c2d9c..6cfe87e 100644 --- a/src/Model/ZipContainer.php +++ b/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; /** diff --git a/tests/CustomZipFormatTest.php b/tests/CustomZipFormatTest.php index 09dc137..020162f 100644 --- a/tests/CustomZipFormatTest.php +++ b/tests/CustomZipFormatTest.php @@ -4,12 +4,16 @@ 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. - * - * @see http://www.epubtest.org/test-books source epub files + **. * * @internal * @@ -19,6 +23,8 @@ final class CustomZipFormatTest extends ZipTestCase { /** * @throws ZipException + * + * @see http://www.epubtest.org/test-books source epub files */ public function testEpub() { @@ -57,4 +63,64 @@ final class CustomZipFormatTest extends ZipTestCase 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); + } + } } diff --git a/tests/Internal/CustomZip/CustomZipWriter.php b/tests/Internal/CustomZip/CustomZipWriter.php new file mode 100644 index 0000000..4d91ee5 --- /dev/null +++ b/tests/Internal/CustomZip/CustomZipWriter.php @@ -0,0 +1,39 @@ +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); + } + } +} diff --git a/tests/Internal/CustomZip/ZipFileCustomWriter.php b/tests/Internal/CustomZip/ZipFileCustomWriter.php new file mode 100644 index 0000000..6c143b0 --- /dev/null +++ b/tests/Internal/CustomZip/ZipFileCustomWriter.php @@ -0,0 +1,20 @@ +zipContainer); + } +} diff --git a/tests/Internal/CustomZip/ZipFileWithBeforeSave.php b/tests/Internal/CustomZip/ZipFileWithBeforeSave.php new file mode 100644 index 0000000..98d470f --- /dev/null +++ b/tests/Internal/CustomZip/ZipFileWithBeforeSave.php @@ -0,0 +1,28 @@ +modify('-1 day'), $now->modify('-10 day')); + $unixExtra = new NewUnixExtraField(); + + foreach ($this->zipContainer->getEntries() as $entry) { + $entry->addExtraField($ntfsTimeExtra); + $entry->addExtraField($unixExtra); + } + } +} diff --git a/tests/ZipEntryTest.php b/tests/ZipEntryTest.php index de9ab52..28ed7c4 100644 --- a/tests/ZipEntryTest.php +++ b/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 * diff --git a/tests/ZipEventTest.php b/tests/ZipEventTest.php deleted file mode 100644 index 63cd426..0000000 --- a/tests/ZipEventTest.php +++ /dev/null @@ -1,41 +0,0 @@ -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(); - } -} diff --git a/tests/ZipFileTest.php b/tests/ZipFileTest.php index b3c83c4..a864fec 100644 --- a/tests/ZipFileTest.php +++ b/tests/ZipFileTest.php @@ -2435,4 +2435,18 @@ class ZipFileTest extends ZipTestCase $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(); + } }