diff --git a/.travis.yml b/.travis.yml index 3ebec66..7256300 100644 --- a/.travis.yml +++ b/.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 diff --git a/composer.json b/composer.json index d360777..7e75f36 100644 --- a/composer.json +++ b/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" diff --git a/phpunit.xml b/phpunit.xml index 046df3b..cbb13f5 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -22,6 +22,12 @@ + + + large + + + src diff --git a/src/Constants/ZipOptions.php b/src/Constants/ZipOptions.php index 25c88a7..c5d9671 100644 --- a/src/Constants/ZipOptions.php +++ b/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'; } diff --git a/src/IO/ZipWriter.php b/src/IO/ZipWriter.php index 5b0a332..b5cc8a8 100644 --- a/src/IO/ZipWriter.php +++ b/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; } /** 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/src/Util/FilesUtil.php b/src/Util/FilesUtil.php index 6b016e8..c29c471 100644 --- a/src/Util/FilesUtil.php +++ b/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 (\DIRECTORY_SEPARATOR === '\\' || !$allowSymlink) { + return file_put_contents($path, $target) !== false; } - if (!symlink($target, $linkPath)) { - if (\DIRECTORY_SEPARATOR === '\\' && is_file($target)) { - return copy($target, $linkPath); - } - - return false; - } - - return true; + return symlink($target, $path); } /** diff --git a/src/ZipFile.php b/src/ZipFile.php index a0ea78f..32560c6 100644 --- a/src/ZipFile.php +++ b/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); - $zipData = null; + $zipEntry->setCreatedOS(ZipPlatform::OS_UNIX); + $zipEntry->setExtractedOS(ZipPlatform::OS_UNIX); - if ($file->isFile()) { + $zipData = null; + $filePerms = $file->getPerms(); + + if ($file->isLink()) { + $linkTarget = $file->getLinkTarget(); + $lengthLinkTarget = \strlen($linkTarget); + + $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(); } /** diff --git a/src/ZipFileInterface.php b/src/ZipFileInterface.php index 1ba1850..07108d1 100644 --- a/src/ZipFileInterface.php +++ b/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. diff --git a/tests/CustomZipFormatTest.php b/tests/CustomZipFormatTest.php new file mode 100644 index 0000000..020162f --- /dev/null +++ b/tests/CustomZipFormatTest.php @@ -0,0 +1,126 @@ +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); + } + } +} diff --git a/tests/Extra/Fields/AbstractUnicodeExtraFieldTest.php b/tests/Extra/Fields/AbstractUnicodeExtraFieldTest.php new file mode 100644 index 0000000..2dd5ee4 --- /dev/null +++ b/tests/Extra/Fields/AbstractUnicodeExtraFieldTest.php @@ -0,0 +1,101 @@ + + */ + 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"); + } +} diff --git a/tests/Extra/Fields/ApkAlignmentExtraFieldTest.php b/tests/Extra/Fields/ApkAlignmentExtraFieldTest.php new file mode 100644 index 0000000..ddc68f5 --- /dev/null +++ b/tests/Extra/Fields/ApkAlignmentExtraFieldTest.php @@ -0,0 +1,106 @@ +getHeaderId(), ApkAlignmentExtraField::HEADER_ID); + self::assertSame($extraField->getMultiple(), $multiple); + self::assertSame($extraField->getPadding(), $padding); + + self::assertSame($extraField->packLocalFileData(), $binaryData); + self::assertSame($extraField->packCentralDirData(), $binaryData); + + self::assertEquals(ApkAlignmentExtraField::unpackLocalFileData($binaryData), $extraField); + self::assertEquals(ApkAlignmentExtraField::unpackCentralDirData($binaryData), $extraField); + + self::assertSame((string) $extraField, $toString); + } + + /** + * @return array + */ + public function provideExtraField() + { + return [ + [ + ApkAlignmentExtraField::ALIGNMENT_BYTES, + 0, + "\x04\x00", + '0xd935 APK Alignment: Multiple=4 Padding=0', + ], + [ + ApkAlignmentExtraField::ALIGNMENT_BYTES, + 1, + "\x04\x00\x00", + '0xd935 APK Alignment: Multiple=4 Padding=1', + ], + [ + ApkAlignmentExtraField::ALIGNMENT_BYTES, + 2, + "\x04\x00\x00\x00", + '0xd935 APK Alignment: Multiple=4 Padding=2', + ], + [ + ApkAlignmentExtraField::ALIGNMENT_BYTES, + 3, + "\x04\x00\x00\x00\x00", + '0xd935 APK Alignment: Multiple=4 Padding=3', + ], + [ + ApkAlignmentExtraField::COMMON_PAGE_ALIGNMENT_BYTES, + 4023, + "\x00\x10\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", + '0xd935 APK Alignment: Multiple=4096 Padding=4023', + ], + ]; + } + + public function testSetter() + { + $extraField = new ApkAlignmentExtraField(ApkAlignmentExtraField::ALIGNMENT_BYTES, 3); + $extraField->setMultiple(ApkAlignmentExtraField::COMMON_PAGE_ALIGNMENT_BYTES); + self::assertSame($extraField->getMultiple(), ApkAlignmentExtraField::COMMON_PAGE_ALIGNMENT_BYTES); + $extraField->setPadding(1); + self::assertSame(1, $extraField->getPadding()); + } + + /** + * @throws ZipException + */ + public function testInvalidParse() + { + $this->setExpectedException( + ZipException::class, + 'Minimum 6 bytes of the extensible data block/field used for alignment of uncompressed entries.' + ); + + ApkAlignmentExtraField::unpackLocalFileData("\x04"); + } +} diff --git a/tests/Extra/Fields/AsiExtraFieldTest.php b/tests/Extra/Fields/AsiExtraFieldTest.php new file mode 100644 index 0000000..5360e87 --- /dev/null +++ b/tests/Extra/Fields/AsiExtraFieldTest.php @@ -0,0 +1,101 @@ +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"); + } +} diff --git a/tests/Extra/Fields/ExtendedTimestampExtraFieldTest.php b/tests/Extra/Fields/ExtendedTimestampExtraFieldTest.php new file mode 100644 index 0000000..0da3af5 --- /dev/null +++ b/tests/Extra/Fields/ExtendedTimestampExtraFieldTest.php @@ -0,0 +1,158 @@ +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); + } +} diff --git a/tests/Extra/Fields/JarMarkerExtraFieldTest.php b/tests/Extra/Fields/JarMarkerExtraFieldTest.php new file mode 100644 index 0000000..2067215 --- /dev/null +++ b/tests/Extra/Fields/JarMarkerExtraFieldTest.php @@ -0,0 +1,55 @@ +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"); + } +} diff --git a/tests/Extra/Fields/NewUnixExtraFieldTest.php b/tests/Extra/Fields/NewUnixExtraFieldTest.php new file mode 100644 index 0000000..02cf159 --- /dev/null +++ b/tests/Extra/Fields/NewUnixExtraFieldTest.php @@ -0,0 +1,97 @@ +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()); + } +} diff --git a/tests/Extra/Fields/NtfsExtraFieldTest.php b/tests/Extra/Fields/NtfsExtraFieldTest.php new file mode 100644 index 0000000..822a51f --- /dev/null +++ b/tests/Extra/Fields/NtfsExtraFieldTest.php @@ -0,0 +1,245 @@ +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); + } +} diff --git a/tests/Extra/Fields/OldUnixExtraFieldTest.php b/tests/Extra/Fields/OldUnixExtraFieldTest.php new file mode 100644 index 0000000..bf0d78a --- /dev/null +++ b/tests/Extra/Fields/OldUnixExtraFieldTest.php @@ -0,0 +1,156 @@ +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); + } +} diff --git a/tests/Extra/Fields/UnicodeCommentExtraFieldTest.php b/tests/Extra/Fields/UnicodeCommentExtraFieldTest.php new file mode 100644 index 0000000..a637111 --- /dev/null +++ b/tests/Extra/Fields/UnicodeCommentExtraFieldTest.php @@ -0,0 +1,42 @@ +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"); + } +} diff --git a/tests/Extra/Fields/WinZipAesExtraFieldTest.php b/tests/Extra/Fields/WinZipAesExtraFieldTest.php new file mode 100644 index 0000000..70e7a69 --- /dev/null +++ b/tests/Extra/Fields/WinZipAesExtraFieldTest.php @@ -0,0 +1,240 @@ +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 + ); + } +} diff --git a/tests/Extra/Fields/Zip64ExtraFieldTest.php b/tests/Extra/Fields/Zip64ExtraFieldTest.php new file mode 100644 index 0000000..815128d --- /dev/null +++ b/tests/Extra/Fields/Zip64ExtraFieldTest.php @@ -0,0 +1,132 @@ +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); + } +} 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/Internal/Epub/EpubFile.php b/tests/Internal/Epub/EpubFile.php new file mode 100644 index 0000000..a30cb76 --- /dev/null +++ b/tests/Internal/Epub/EpubFile.php @@ -0,0 +1,98 @@ +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; + } +} diff --git a/tests/Internal/Epub/EpubInfo.php b/tests/Internal/Epub/EpubInfo.php new file mode 100644 index 0000000..e1e1b78 --- /dev/null +++ b/tests/Internal/Epub/EpubInfo.php @@ -0,0 +1,159 @@ +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, + ]; + } +} diff --git a/tests/Internal/Epub/EpubReader.php b/tests/Internal/Epub/EpubReader.php new file mode 100644 index 0000000..ba909bb --- /dev/null +++ b/tests/Internal/Epub/EpubReader.php @@ -0,0 +1,21 @@ +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()); + } + ); + } +} diff --git a/tests/Internal/Epub/EpubZipContainer.php b/tests/Internal/Epub/EpubZipContainer.php new file mode 100644 index 0000000..29fff15 --- /dev/null +++ b/tests/Internal/Epub/EpubZipContainer.php @@ -0,0 +1,22 @@ +getEntry('mimetype')->getData()->getDataAsString(); + } +} diff --git a/tests/SymlinkTest.php b/tests/SymlinkTest.php new file mode 100644 index 0000000..2731607 --- /dev/null +++ b/tests/SymlinkTest.php @@ -0,0 +1,88 @@ +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]; + } +} diff --git a/tests/Zip64Test.php b/tests/Zip64Test.php index a87752f..d99e91f 100644 --- a/tests/Zip64Test.php +++ b/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(); diff --git a/tests/ZipEntryTest.php b/tests/ZipEntryTest.php index 4a32e60..7bb46b1 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 7782916..4230f7b 100644 --- a/tests/ZipFileTest.php +++ b/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 diff --git a/tests/ZipInfoTest.php b/tests/ZipInfoTest.php new file mode 100644 index 0000000..67957d6 --- /dev/null +++ b/tests/ZipInfoTest.php @@ -0,0 +1,118 @@ +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}'); + } +} diff --git a/tests/resources/Advanced-v1.0.0.epub b/tests/resources/Advanced-v1.0.0.epub new file mode 100644 index 0000000..d12f996 Binary files /dev/null and b/tests/resources/Advanced-v1.0.0.epub differ