From a16b0e7c15b2d86fc806b8d356c5fc27a0be11f7 Mon Sep 17 00:00:00 2001 From: wapplay Date: Mon, 6 Jan 2020 11:53:17 +0300 Subject: [PATCH] minor fixes, ZipEntry tests --- .gitattributes | 3 +- .phpstorm.meta.php | 52 +- bootstrap.php | 6 + phpunit.xml | 2 +- .../PKCryptContext.php | 2 +- .../PKDecryptionStreamFilter.php | 2 +- .../PKEncryptionStreamFilter.php | 4 +- src/IO/ZipReader.php | 4 +- src/IO/ZipWriter.php | 6 +- src/Model/Data/ZipSourceFileData.php | 25 +- src/Model/Extra/Fields/NtfsExtraField.php | 38 +- src/Model/ZipContainer.php | 28 - src/Model/ZipEntry.php | 193 +- src/Util/DateTimeConverter.php | 55 +- src/Util/StringUtil.php | 20 +- src/ZipFile.php | 6 +- tests/SlowTests/Zip64Test.php | 2 - tests/Zip64Test.php | 6 + tests/ZipEntryTest.php | 1561 +++++++++++++++++ tests/ZipFileTest.php | 29 + tests/ZipPasswordTest.php | 3 +- 21 files changed, 1906 insertions(+), 141 deletions(-) create mode 100644 bootstrap.php rename src/IO/Filter/Cipher/{Traditional => Pkware}/PKCryptContext.php (99%) rename src/IO/Filter/Cipher/{Traditional => Pkware}/PKDecryptionStreamFilter.php (98%) rename src/IO/Filter/Cipher/{Traditional => Pkware}/PKEncryptionStreamFilter.php (95%) create mode 100644 tests/ZipEntryTest.php diff --git a/.gitattributes b/.gitattributes index 53d0202..a76f41e 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,7 +1,8 @@ .gitattributes export-ignore .github export-ignore .gitignore export-ignore -.travis.yml export-ignore .php_cs export-ignore +.travis.yml export-ignore +bootstrap.php export-ignore phpunit.xml export-ignore tests export-ignore diff --git a/.phpstorm.meta.php b/.phpstorm.meta.php index 3b911f9..516aa2d 100644 --- a/.phpstorm.meta.php +++ b/.phpstorm.meta.php @@ -26,6 +26,8 @@ namespace PHPSTORM_META { expectedArguments(\PhpZip\ZipFile::addFilesFromRegex(), 3, argumentsSet("compression_methods")); expectedArguments(\PhpZip\ZipFile::addFilesFromRegexRecursive(), 3, argumentsSet("compression_methods")); expectedArguments(\PhpZip\ZipFile::setCompressionMethodEntry(), 1, argumentsSet("compression_methods")); + expectedArguments(\PhpZip\Model\ZipEntry::setCompressionMethod(), 0, argumentsSet("compression_methods")); + expectedArguments(\PhpZip\Model\ZipEntry::setMethod(), 0, argumentsSet("compression_methods")); registerArgumentsSet( 'compression_levels', @@ -36,6 +38,7 @@ namespace PHPSTORM_META { ); expectedArguments(\PhpZip\ZipFile::setCompressionLevel(), 0, argumentsSet("compression_levels")); expectedArguments(\PhpZip\ZipFile::setCompressionLevelEntry(), 1, argumentsSet("compression_levels")); + expectedArguments(\PhpZip\Model\ZipEntry::setCompressionLevel(), 0, argumentsSet("compression_levels")); registerArgumentsSet( 'encryption_methods', @@ -46,6 +49,8 @@ namespace PHPSTORM_META { ); expectedArguments(\PhpZip\ZipFile::setPassword(), 1, argumentsSet("encryption_methods")); expectedArguments(\PhpZip\ZipFile::setPasswordEntry(), 2, argumentsSet("encryption_methods")); + expectedArguments(\PhpZip\Model\ZipEntry::setEncryptionMethod(), 0, argumentsSet("encryption_methods")); + expectedArguments(\PhpZip\Model\ZipEntry::setPassword(), 1, argumentsSet("encryption_methods")); registerArgumentsSet( 'zip_mime_types', @@ -57,6 +62,49 @@ namespace PHPSTORM_META { expectedArguments(\PhpZip\ZipFile::outputAsAttachment(), 1, argumentsSet("zip_mime_types")); expectedArguments(\PhpZip\ZipFile::outputAsAttachment(), 2, argumentsSet("bool")); - expectedArguments(\PhpZip\ZipFileI::outputAsResponse(), 2, argumentsSet("zip_mime_types")); - expectedArguments(\PhpZip\ZipFileI::outputAsResponse(), 3, argumentsSet("bool")); + expectedArguments(\PhpZip\ZipFile::outputAsResponse(), 2, argumentsSet("zip_mime_types")); + expectedArguments(\PhpZip\ZipFile::outputAsResponse(), 3, argumentsSet("bool")); + + registerArgumentsSet( + 'dos_charset', + \PhpZip\Constants\DosCodePage::CP_LATIN_US, + \PhpZip\Constants\DosCodePage::CP_GREEK, + \PhpZip\Constants\DosCodePage::CP_BALT_RIM, + \PhpZip\Constants\DosCodePage::CP_LATIN1, + \PhpZip\Constants\DosCodePage::CP_LATIN2, + \PhpZip\Constants\DosCodePage::CP_CYRILLIC, + \PhpZip\Constants\DosCodePage::CP_TURKISH, + \PhpZip\Constants\DosCodePage::CP_PORTUGUESE, + \PhpZip\Constants\DosCodePage::CP_ICELANDIC, + \PhpZip\Constants\DosCodePage::CP_HEBREW, + \PhpZip\Constants\DosCodePage::CP_CANADA, + \PhpZip\Constants\DosCodePage::CP_ARABIC, + \PhpZip\Constants\DosCodePage::CP_NORDIC, + \PhpZip\Constants\DosCodePage::CP_CYRILLIC_RUSSIAN, + \PhpZip\Constants\DosCodePage::CP_GREEK2, + \PhpZip\Constants\DosCodePage::CP_THAI, + ); + expectedArguments(\PhpZip\Model\ZipEntry::setCharset(), 0, argumentsSet('dos_charset')); + expectedArguments(\PhpZip\Constants\DosCodePage::toUTF8(), 1, argumentsSet('dos_charset')); + expectedArguments(\PhpZip\Constants\DosCodePage::fromUTF8(), 1, argumentsSet('dos_charset')); + + registerArgumentsSet( + "zip_os", + \PhpZip\Constants\ZipPlatform::OS_UNIX, + \PhpZip\Constants\ZipPlatform::OS_DOS, + \PhpZip\Constants\ZipPlatform::OS_MAC_OSX, + ); + expectedArguments(\PhpZip\Model\ZipEntry::setCreatedOS(), 0, argumentsSet('zip_os')); + expectedArguments(\PhpZip\Model\ZipEntry::setExtractedOS(), 0, argumentsSet('zip_os')); + expectedArguments(\PhpZip\Model\ZipEntry::setPlatform(), 0, argumentsSet('zip_os')); + + registerArgumentsSet( + "zip_gpbf", + \PhpZip\Constants\GeneralPurposeBitFlag::ENCRYPTION | + \PhpZip\Constants\GeneralPurposeBitFlag::DATA_DESCRIPTOR | + \PhpZip\Constants\GeneralPurposeBitFlag::COMPRESSION_FLAG1 | + \PhpZip\Constants\GeneralPurposeBitFlag::COMPRESSION_FLAG2 | + \PhpZip\Constants\GeneralPurposeBitFlag::UTF8 + ); + expectedArguments(\PhpZip\Model\ZipEntry::setGeneralPurposeBitFlags(), 0, argumentsSet('zip_gpbf')); } diff --git a/bootstrap.php b/bootstrap.php new file mode 100644 index 0000000..1b1b713 --- /dev/null +++ b/bootstrap.php @@ -0,0 +1,6 @@ + + bootstrap="bootstrap.php"> diff --git a/src/IO/Filter/Cipher/Traditional/PKCryptContext.php b/src/IO/Filter/Cipher/Pkware/PKCryptContext.php similarity index 99% rename from src/IO/Filter/Cipher/Traditional/PKCryptContext.php rename to src/IO/Filter/Cipher/Pkware/PKCryptContext.php index ad42457..bffa4d6 100644 --- a/src/IO/Filter/Cipher/Traditional/PKCryptContext.php +++ b/src/IO/Filter/Cipher/Pkware/PKCryptContext.php @@ -1,6 +1,6 @@ context = new PKCryptContext($password); - $crc = $entry->isDataDescriptorRequired() ? + $crc = $entry->isDataDescriptorRequired() || $entry->getCrc() === ZipEntry::UNKNOWN ? ($entry->getDosTime() & 0x0000ffff) << 16 : $entry->getCrc(); diff --git a/src/IO/ZipReader.php b/src/IO/ZipReader.php index 54d422f..7a46f2d 100644 --- a/src/IO/ZipReader.php +++ b/src/IO/ZipReader.php @@ -11,7 +11,7 @@ use PhpZip\Constants\ZipOptions; use PhpZip\Exception\Crc32Exception; use PhpZip\Exception\InvalidArgumentException; use PhpZip\Exception\ZipException; -use PhpZip\IO\Filter\Cipher\Traditional\PKDecryptionStreamFilter; +use PhpZip\IO\Filter\Cipher\Pkware\PKDecryptionStreamFilter; use PhpZip\IO\Filter\Cipher\WinZipAes\WinZipAesDecryptionStreamFilter; use PhpZip\Model\Data\ZipSourceFileData; use PhpZip\Model\EndOfCentralDirectory; @@ -713,7 +713,7 @@ class ZipReader throw new InvalidArgumentException('outStream is not resource'); } - $entry = $zipFileData->getZipEntry(); + $entry = $zipFileData->getSourceEntry(); // if ($entry->isDirectory()) { // throw new InvalidArgumentException('Streams not supported for directories'); diff --git a/src/IO/ZipWriter.php b/src/IO/ZipWriter.php index c499f4b..5b0a332 100644 --- a/src/IO/ZipWriter.php +++ b/src/IO/ZipWriter.php @@ -10,7 +10,7 @@ use PhpZip\Constants\ZipPlatform; use PhpZip\Constants\ZipVersion; use PhpZip\Exception\ZipException; use PhpZip\Exception\ZipUnsupportMethodException; -use PhpZip\IO\Filter\Cipher\Traditional\PKEncryptionStreamFilter; +use PhpZip\IO\Filter\Cipher\Pkware\PKEncryptionStreamFilter; use PhpZip\IO\Filter\Cipher\WinZipAes\WinZipAesEncryptionStreamFilter; use PhpZip\Model\Data\ZipSourceFileData; use PhpZip\Model\Extra\Fields\ApkAlignmentExtraField; @@ -109,7 +109,6 @@ class ZipWriter $compressedSize = $entry->getCompressedSize(); $uncompressedSize = $entry->getUncompressedSize(); - // todo check on 32bit system $entry->getLocalExtraFields()->remove(Zip64ExtraField::HEADER_ID); if ($compressedSize > ZipConstants::ZIP64_MAGIC || $uncompressedSize > ZipConstants::ZIP64_MAGIC) { @@ -331,7 +330,7 @@ class ZipWriter // (PHP cannot apply the filter for encryption after the compression // filter, so a temporary stream is created for the compressed data) - if ($zipData instanceof ZipSourceFileData && !$this->zipContainer->hasRecompressData($entry)) { + if ($zipData instanceof ZipSourceFileData && !$zipData->hasRecompressData($entry)) { // data of source zip file -> copy compressed data $zipData->copyCompressedDataToStream($outStream); @@ -631,7 +630,6 @@ class ZipWriter $uncompressedSize = $entry->getUncompressedSize(); $localHeaderOffset = $entry->getLocalHeaderOffset(); - // todo check on 32bit system $entry->getCdExtraFields()->remove(Zip64ExtraField::HEADER_ID); if ( diff --git a/src/Model/Data/ZipSourceFileData.php b/src/Model/Data/ZipSourceFileData.php index fc2328f..c53df05 100644 --- a/src/Model/Data/ZipSourceFileData.php +++ b/src/Model/Data/ZipSourceFileData.php @@ -20,7 +20,7 @@ class ZipSourceFileData implements ZipData private $stream; /** @var ZipEntry */ - private $zipEntry; + private $sourceEntry; /** @var int */ private $offset; @@ -42,11 +42,28 @@ class ZipSourceFileData implements ZipData { $this->zipReader = $zipReader; $this->offset = $offsetData; - $this->zipEntry = $zipEntry; + $this->sourceEntry = $zipEntry; $this->compressedSize = $zipEntry->getCompressedSize(); $this->uncompressedSize = $zipEntry->getUncompressedSize(); } + /** + * @param ZipEntry $entry + * + * @return bool + */ + public function hasRecompressData(ZipEntry $entry) + { + return $this->sourceEntry->getCompressionLevel() !== $entry->getCompressionLevel() || + $this->sourceEntry->getCompressionMethod() !== $entry->getCompressionMethod() || + $this->sourceEntry->isEncrypted() !== $entry->isEncrypted() || + $this->sourceEntry->getEncryptionMethod() !== $entry->getEncryptionMethod() || + $this->sourceEntry->getPassword() !== $entry->getPassword() || + $this->sourceEntry->getCompressedSize() !== $entry->getCompressedSize() || + $this->sourceEntry->getUncompressedSize() !== $entry->getUncompressedSize() || + $this->sourceEntry->getCrc() !== $entry->getCrc(); + } + /** * @throws ZipException * @@ -114,9 +131,9 @@ class ZipSourceFileData implements ZipData /** * @return ZipEntry */ - public function getZipEntry() + public function getSourceEntry() { - return $this->zipEntry; + return $this->sourceEntry; } /** diff --git a/src/Model/Extra/Fields/NtfsExtraField.php b/src/Model/Extra/Fields/NtfsExtraField.php index 7d68d78..2cc7d59 100644 --- a/src/Model/Extra/Fields/NtfsExtraField.php +++ b/src/Model/Extra/Fields/NtfsExtraField.php @@ -33,13 +33,13 @@ class NtfsExtraField implements ZipExtraField */ const EPOCH_OFFSET = -11644473600; - /** @var int Modify time timestamp */ + /** @var int Modify ntfs time */ private $modifyTime; - /** @var int Access time timestamp */ + /** @var int Access ntfs time */ private $accessTime; - /** @var int Create time timestamp */ + /** @var int Create ntfs time */ private $createTime; /** @@ -54,6 +54,22 @@ class NtfsExtraField implements ZipExtraField $this->createTime = (int) $createTime; } + /** + * @param \DateTimeInterface $mtime + * @param \DateTimeInterface $atime + * @param \DateTimeInterface $ctime + * + * @return NtfsExtraField + */ + public static function create(\DateTimeInterface $mtime, \DateTimeInterface $atime, \DateTimeInterface $ctime) + { + return new self( + self::dateTimeToNtfsTime($mtime), + self::dateTimeToNtfsTime($atime), + self::dateTimeToNtfsTime($ctime) + ); + } + /** * Returns the Header ID (type) of this Extra Field. * The Header ID is an unsigned short integer (two bytes) @@ -145,7 +161,7 @@ class NtfsExtraField implements ZipExtraField */ public function getModifyDateTime() { - return $this->ntfsTimeToDateTime($this->modifyTime); + return self::ntfsTimeToDateTime($this->modifyTime); } /** @@ -153,7 +169,7 @@ class NtfsExtraField implements ZipExtraField */ public function setModifyDateTime(\DateTimeInterface $modifyTime) { - $this->modifyTime = $this->dateTimeToNtfsTime($modifyTime); + $this->modifyTime = self::dateTimeToNtfsTime($modifyTime); } /** @@ -161,7 +177,7 @@ class NtfsExtraField implements ZipExtraField */ public function getAccessDateTime() { - return $this->ntfsTimeToDateTime($this->accessTime); + return self::ntfsTimeToDateTime($this->accessTime); } /** @@ -169,7 +185,7 @@ class NtfsExtraField implements ZipExtraField */ public function setAccessDateTime(\DateTimeInterface $accessTime) { - $this->accessTime = $this->dateTimeToNtfsTime($accessTime); + $this->accessTime = self::dateTimeToNtfsTime($accessTime); } /** @@ -177,7 +193,7 @@ class NtfsExtraField implements ZipExtraField */ public function getCreateDateTime() { - return $this->ntfsTimeToDateTime($this->createTime); + return self::ntfsTimeToDateTime($this->createTime); } /** @@ -185,7 +201,7 @@ class NtfsExtraField implements ZipExtraField */ public function setCreateDateTime(\DateTimeInterface $createTime) { - $this->createTime = $this->dateTimeToNtfsTime($createTime); + $this->createTime = self::dateTimeToNtfsTime($createTime); } /** @@ -193,7 +209,7 @@ class NtfsExtraField implements ZipExtraField * * @return int */ - protected function dateTimeToNtfsTime(\DateTimeInterface $dateTime) + public static function dateTimeToNtfsTime(\DateTimeInterface $dateTime) { return $dateTime->getTimestamp() * 10000000 + self::EPOCH_OFFSET; } @@ -203,7 +219,7 @@ class NtfsExtraField implements ZipExtraField * * @return \DateTimeInterface */ - protected function ntfsTimeToDateTime($time) + public static function ntfsTimeToDateTime($time) { $timestamp = (int) ($time / 10000000 + self::EPOCH_OFFSET); diff --git a/src/Model/ZipContainer.php b/src/Model/ZipContainer.php index 1f502ad..f3c2d9c 100644 --- a/src/Model/ZipContainer.php +++ b/src/Model/ZipContainer.php @@ -281,34 +281,6 @@ class ZipContainer extends ImmutableZipContainer $this->archiveComment = $archiveComment; } - /** - * @param ZipEntry $entry - * - * @return bool - */ - public function hasRecompressData(ZipEntry $entry) - { - // todo test with rename, check exists ZipSourceData - if ($this->sourceContainer && isset($this->sourceContainer->entries[$entry->getName()])) { - $sourceEntry = $this->sourceContainer->entries[$entry->getName()]; - - if ( - $sourceEntry->getCompressionLevel() !== $entry->getCompressionLevel() || - $sourceEntry->getCompressionMethod() !== $entry->getCompressionMethod() || - $sourceEntry->isEncrypted() !== $entry->isEncrypted() || - $sourceEntry->getEncryptionMethod() !== $entry->getEncryptionMethod() || - $sourceEntry->getPassword() !== $entry->getPassword() || - $sourceEntry->getCompressedSize() !== $entry->getCompressedSize() || - $sourceEntry->getUncompressedSize() !== $entry->getUncompressedSize() || - $sourceEntry->getCrc() !== $entry->getCrc() - ) { - return true; - } - } - - return false; - } - /** * @return ZipEntryMatcher */ diff --git a/src/Model/ZipEntry.php b/src/Model/ZipEntry.php index 51bb7c5..44e6448 100644 --- a/src/Model/ZipEntry.php +++ b/src/Model/ZipEntry.php @@ -31,9 +31,9 @@ use PhpZip\Util\StringUtil; /** * ZIP file entry. * - * @see https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT .ZIP File Format Specification + * @see https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT .ZIP File Format Specification * - * @author Ne-Lexa alexey@nelexa.ru + * @author Ne-Lexa alexey@nelexa.ru * @license MIT */ class ZipEntry @@ -152,11 +152,12 @@ class ZipEntry /** * ZipEntry constructor. * - * @param string $name Entry name + * @param string $name Entry name + * @param string|null $charset DOS charset */ - public function __construct($name) + public function __construct($name, $charset = null) { - $this->setName($name); + $this->setName($name, $charset); $this->cdExtraFields = new ExtraFieldsCollection(); $this->localExtraFields = new ExtraFieldsCollection(); @@ -185,6 +186,7 @@ class ZipEntry * @return ZipEntry * * @internal + * * @noinspection PhpTooManyParametersInspection */ public static function create( @@ -229,11 +231,12 @@ class ZipEntry /** * Set entry name. * - * @param string $name New entry name + * @param string $name New entry name + * @param string|null $charset * * @return ZipEntry */ - private function setName($name) + private function setName($name, $charset = null) { if ($name === null) { throw new InvalidArgumentException('zip entry name is null'); @@ -252,13 +255,24 @@ class ZipEntry throw new InvalidArgumentException('Illegal zip entry name parameter'); } - if (!StringUtil::isASCII($name)) { + $this->setCharset($charset); + + if ($this->charset === null && !StringUtil::isASCII($name)) { $this->enableUtf8Name(true); } $this->name = $name; $this->isDirectory = ($length = \strlen($name)) >= 1 && $name[$length - 1] === '/'; $this->externalAttributes = $this->isDirectory ? DosAttrs::DOS_DIRECTORY : DosAttrs::DOS_ARCHIVE; + if ($this->extractVersion !== self::UNKNOWN) { + $this->extractVersion = max( + $this->extractVersion, + $this->isDirectory ? + ZipVersion::v20_DEFLATED_FOLDER_ZIPCRYPTO : + ZipVersion::v10_DEFAULT_MIN + ); + } + return $this; } @@ -291,6 +305,8 @@ class ZipEntry * @param string $newName New entry name * * @return ZipEntry new {@see ZipEntry} object with new name + * + * @internal */ public function rename($newName) { @@ -314,6 +330,8 @@ class ZipEntry /** * @return ZipData|null + * + * @internal */ public function getData() { @@ -322,6 +340,8 @@ class ZipEntry /** * @param ZipData|null $data + * + * @internal */ public function setData($data) { @@ -458,7 +478,7 @@ class ZipEntry return ZipVersion::v51_ENCR_AES_RC2_CORRECT; } - if ($this->getCompressionMethod() === ZipCompressionMethod::BZIP2) { + if ($this->compressionMethod === ZipCompressionMethod::BZIP2) { return ZipVersion::v46_BZIP2; } @@ -466,9 +486,15 @@ class ZipEntry return ZipVersion::v45_ZIP64_EXT; } - return $this->getCompressionMethod() === ZipCompressionMethod::DEFLATED || $this->isDirectory() ? - ZipVersion::v20_DEFLATED_FOLDER_ZIPCRYPTO : - ZipVersion::v10_DEFAULT_MIN; + if ( + $this->compressionMethod === ZipCompressionMethod::DEFLATED || + $this->isDirectory || + $this->encryptionMethod === ZipEncryptionMethod::PKWARE + ) { + return ZipVersion::v20_DEFLATED_FOLDER_ZIPCRYPTO; + } + + return ZipVersion::v10_DEFAULT_MIN; } return $this->extractVersion; @@ -520,9 +546,16 @@ class ZipEntry * @param int $compressedSize the Compressed Size * * @return ZipEntry + * + * @internal */ public function setCompressedSize($compressedSize) { + $compressedSize = (int) $compressedSize; + + if ($compressedSize < self::UNKNOWN) { + throw new InvalidArgumentException('Compressed size < ' . self::UNKNOWN); + } $this->compressedSize = $compressedSize; return $this; @@ -550,6 +583,8 @@ class ZipEntry * @return ZipEntry * * @deprecated Use {@see ZipEntry::setUncompressedSize()} + * + * @internal */ public function setSize($size) { @@ -574,9 +609,16 @@ class ZipEntry * @param int $uncompressedSize the (Uncompressed) Size * * @return ZipEntry + * + * @internal */ public function setUncompressedSize($uncompressedSize) { + $uncompressedSize = (int) $uncompressedSize; + + if ($uncompressedSize < self::UNKNOWN) { + throw new InvalidArgumentException('Uncompressed size < ' . self::UNKNOWN); + } $this->uncompressedSize = $uncompressedSize; return $this; @@ -596,10 +638,17 @@ class ZipEntry * @param int $localHeaderOffset * * @return ZipEntry + * + * @internal */ public function setLocalHeaderOffset($localHeaderOffset) { - $this->localHeaderOffset = (int) $localHeaderOffset; + $localHeaderOffset = (int) $localHeaderOffset; + + if ($localHeaderOffset < 0) { + throw new InvalidArgumentException('Negative $localHeaderOffset'); + } + $this->localHeaderOffset = $localHeaderOffset; return $this; } @@ -627,6 +676,8 @@ class ZipEntry * @return ZipEntry * * @deprecated Use {@see ZipEntry::setLocalHeaderOffset()} + * + * @internal */ public function setOffset($offset) { @@ -645,24 +696,26 @@ class ZipEntry */ public function getGeneralPurposeBitFlags() { - return $this->generalPurposeBitFlags & 0xffff; + return $this->generalPurposeBitFlags; } /** * Sets the General Purpose Bit Flags. * - * @param mixed $general + * @param int $gpbf general purpose bit flags * * @return ZipEntry * - * @var int general + * @internal */ - public function setGeneralPurposeBitFlags($general) + public function setGeneralPurposeBitFlags($gpbf) { - if ($general < 0x0000 || $general > 0xffff) { - throw new InvalidArgumentException('general out of range'); + $gpbf = (int) $gpbf; + + if ($gpbf < 0x0000 || $gpbf > 0xffff) { + throw new InvalidArgumentException('general purpose bit flags out of range'); } - $this->generalPurposeBitFlags = $general; + $this->generalPurposeBitFlags = $gpbf; $this->updateCompressionLevel(); return $this; @@ -710,7 +763,7 @@ class ZipEntry */ private function isSetGeneralBitFlag($mask) { - return ($this->generalPurposeBitFlags & $mask) !== 0; + return ($this->generalPurposeBitFlags & $mask) === $mask; } /** @@ -775,7 +828,9 @@ class ZipEntry { $this->setEncrypted(false); $this->removeExtraField(WinZipAesExtraField::HEADER_ID); + $this->encryptionMethod = ZipEncryptionMethod::NONE; $this->password = null; + $this->extractVersion = self::UNKNOWN; return $this; } @@ -858,7 +913,9 @@ class ZipEntry */ public function setCompressionMethod($compressionMethod) { - if (($compressionMethod < 0x0000 || $compressionMethod > 0xffff) && $compressionMethod !== self::UNKNOWN) { + $compressionMethod = (int) $compressionMethod; + + if ($compressionMethod < 0x0000 || $compressionMethod > 0xffff) { throw new InvalidArgumentException('method out of range: ' . $compressionMethod); } @@ -866,6 +923,7 @@ class ZipEntry $this->compressionMethod = $compressionMethod; $this->updateCompressionLevel(); + $this->extractVersion = self::UNKNOWN; return $this; } @@ -881,7 +939,7 @@ class ZipEntry return self::UNKNOWN; } - return DateTimeConverter::toUnixTimestamp($this->getDosTime()); + return DateTimeConverter::msDosToUnix($this->getDosTime()); } /** @@ -922,10 +980,8 @@ class ZipEntry */ public function setTime($unixTimestamp) { - $known = $unixTimestamp !== self::UNKNOWN; - - if ($known) { - $this->dosTime = DateTimeConverter::toDosTime($unixTimestamp); + if ($unixTimestamp !== self::UNKNOWN) { + $this->setDosTime(DateTimeConverter::unixToMsDos($unixTimestamp)); } else { $this->dosTime = 0; } @@ -954,6 +1010,11 @@ class ZipEntry { $this->externalAttributes = (int) $externalAttributes; + if ($externalAttributes < 0x00000000 || $externalAttributes > 0xffffffff) { + throw new InvalidArgumentException('external attributes out of range: ' . $externalAttributes); + } + $this->externalAttributes = $externalAttributes; + return $this; } @@ -970,13 +1031,18 @@ class ZipEntry /** * Sets the internal file attributes. * - * @param int $attributes the internal file attributes + * @param int $internalAttributes the internal file attributes * * @return ZipEntry */ - public function setInternalAttributes($attributes) + public function setInternalAttributes($internalAttributes) { - $this->internalAttributes = (int) $attributes; + $internalAttributes = (int) $internalAttributes; + + if ($internalAttributes < 0x0000 || $internalAttributes > 0xffff) { + throw new InvalidArgumentException('internal attributes out of range'); + } + $this->internalAttributes = $internalAttributes; return $this; } @@ -1094,6 +1160,31 @@ class ZipEntry $this->localExtraFields->remove($headerId); } + /** + * @param ZipExtraField $zipExtraField + */ + public function addExtraField(ZipExtraField $zipExtraField) + { + $this->addLocalExtraField($zipExtraField); + $this->addCdExtraField($zipExtraField); + } + + /** + * @param ZipExtraField $zipExtraField + */ + public function addLocalExtraField(ZipExtraField $zipExtraField) + { + $this->localExtraFields->add($zipExtraField); + } + + /** + * @param ZipExtraField $zipExtraField + */ + public function addCdExtraField(ZipExtraField $zipExtraField) + { + $this->cdExtraFields->add($zipExtraField); + } + /** * Returns comment entry. * @@ -1116,11 +1207,11 @@ class ZipEntry if ($comment !== null) { $commentLength = \strlen($comment); - if ($commentLength < 0x0000 || $commentLength > 0xffff) { + if ($commentLength > 0xffff) { throw new InvalidArgumentException('Comment too long'); } - if (!StringUtil::isASCII($comment)) { + if ($this->charset === null && !StringUtil::isASCII($comment)) { $this->enableUtf8Name(true); } } @@ -1153,6 +1244,8 @@ class ZipEntry * @param int $crc * * @return ZipEntry + * + * @internal */ public function setCrc($crc) { @@ -1180,15 +1273,19 @@ class ZipEntry public function setPassword($password, $encryptionMethod = null) { if (!$this->isDirectory) { - if ($encryptionMethod !== null) { - $this->setEncryptionMethod($encryptionMethod); - } - if ($password === null || $password === '') { $this->password = null; $this->disableEncryption(); } else { $this->password = (string) $password; + + if ($encryptionMethod === null && $this->encryptionMethod === ZipEncryptionMethod::NONE) { + $encryptionMethod = ZipEncryptionMethod::WINZIP_AES_256; + } + + if ($encryptionMethod !== null) { + $this->setEncryptionMethod($encryptionMethod); + } $this->setEncrypted(true); } } @@ -1228,6 +1325,7 @@ class ZipEntry $this->encryptionMethod = $encryptionMethod; $this->setEncrypted($this->encryptionMethod !== ZipEncryptionMethod::NONE); + $this->extractVersion = self::UNKNOWN; return $this; } @@ -1329,18 +1427,21 @@ class ZipEntry */ public function getUnixMode() { - /** @var AsiExtraField|null $asiExtraField */ - $asiExtraField = $this->getExtraField(AsiExtraField::HEADER_ID); - - if ($asiExtraField !== null) { - return $asiExtraField->getMode(); - } + $mode = 0; if ($this->createdOS === ZipPlatform::OS_UNIX) { - return ($this->externalAttributes >> 16) & 0xFFFF; + $mode = ($this->externalAttributes >> 16) & 0xFFFF; + } elseif ($this->hasExtraField(AsiExtraField::HEADER_ID)) { + /** @var AsiExtraField $asiExtraField */ + $asiExtraField = $this->getExtraField(AsiExtraField::HEADER_ID); + $mode = $asiExtraField->getMode(); } - return $this->isDirectory ? 040755 : 0100664; + if ($mode > 0) { + return $mode; + } + + return $this->isDirectory ? 040755 : 0100644; } /** @@ -1351,8 +1452,8 @@ class ZipEntry */ public function isZip64ExtensionsRequired() { - return $this->compressedSize >= ZipConstants::ZIP64_MAGIC - || $this->uncompressedSize >= ZipConstants::ZIP64_MAGIC; + return $this->compressedSize > ZipConstants::ZIP64_MAGIC + || $this->uncompressedSize > ZipConstants::ZIP64_MAGIC; } /** diff --git a/src/Util/DateTimeConverter.php b/src/Util/DateTimeConverter.php index eb65c30..615ba22 100644 --- a/src/Util/DateTimeConverter.php +++ b/src/Util/DateTimeConverter.php @@ -5,6 +5,21 @@ namespace PhpZip\Util; /** * Convert unix timestamp values to DOS date/time values and vice versa. * + * The DOS date/time format is a bitmask: + * + * 24 16 8 0 + * +-+-+-+-+-+-+-+-+ +-+-+-+-+-+-+-+-+ +-+-+-+-+-+-+-+-+ +-+-+-+-+-+-+-+-+ + * |Y|Y|Y|Y|Y|Y|Y|M| |M|M|M|D|D|D|D|D| |h|h|h|h|h|m|m|m| |m|m|m|s|s|s|s|s| + * +-+-+-+-+-+-+-+-+ +-+-+-+-+-+-+-+-+ +-+-+-+-+-+-+-+-+ +-+-+-+-+-+-+-+-+ + * \___________/\________/\_________/ \________/\____________/\_________/ + * year month day hour minute second + * + * The year is stored as an offset from 1980. + * Seconds are stored in two-second increments. + * (So if the "second" value is 15, it actually represents 30 seconds.) + * + * @see https://docs.microsoft.com/ru-ru/windows/win32/api/winbase/nf-winbase-filetimetodosdatetime?redirectedfrom=MSDN + * * @author Ne-Lexa alexey@nelexa.ru * @license MIT * @@ -31,21 +46,21 @@ class DateTimeConverter * * @return int Unix timestamp */ - public static function toUnixTimestamp($dosTime) + public static function msDosToUnix($dosTime) { - if ($dosTime < self::MIN_DOS_TIME) { - $dosTime = self::MIN_DOS_TIME; + if ($dosTime <= self::MIN_DOS_TIME) { + $dosTime = 0; } elseif ($dosTime > self::MAX_DOS_TIME) { $dosTime = self::MAX_DOS_TIME; } - +// date_default_timezone_set('UTC'); return mktime( - ($dosTime >> 11) & 0x1f, // hour - ($dosTime >> 5) & 0x3f, // minute - 2 * ($dosTime & 0x1f), // second - ($dosTime >> 21) & 0x0f, // month - ($dosTime >> 16) & 0x1f, // day - 1980 + (($dosTime >> 25) & 0x7f) // year + (($dosTime >> 11) & 0x1f), // hours + (($dosTime >> 5) & 0x3f), // minutes + (($dosTime << 1) & 0x3e), // seconds + (($dosTime >> 21) & 0x0f), // month + (($dosTime >> 16) & 0x1f), // day + ((($dosTime >> 25) & 0x7f) + 1980) // year ); } @@ -59,22 +74,26 @@ class DateTimeConverter * rounded down to even seconds * and is in between DateTimeConverter::MIN_DOS_TIME and DateTimeConverter::MAX_DOS_TIME */ - public static function toDosTime($unixTimestamp) + public static function unixToMsDos($unixTimestamp) { if ($unixTimestamp < 0) { throw new \InvalidArgumentException('Negative unix timestamp: ' . $unixTimestamp); } $date = getdate($unixTimestamp); + $dosTime = ( + (($date['year'] - 1980) << 25) | + ($date['mon'] << 21) | + ($date['mday'] << 16) | + ($date['hours'] << 11) | + ($date['minutes'] << 5) | + ($date['seconds'] >> 1) + ); - if ($date['year'] < 1980) { - return self::MIN_DOS_TIME; + if ($dosTime <= self::MIN_DOS_TIME) { + $dosTime = 0; } - $date['year'] -= 1980; - - return $date['year'] << 25 | $date['mon'] << 21 | - $date['mday'] << 16 | $date['hours'] << 11 | - $date['minutes'] << 5 | $date['seconds'] >> 1; + return $dosTime; } } diff --git a/src/Util/StringUtil.php b/src/Util/StringUtil.php index c40a8ad..bce2a17 100644 --- a/src/Util/StringUtil.php +++ b/src/Util/StringUtil.php @@ -9,17 +9,6 @@ namespace PhpZip\Util; */ final class StringUtil { - /** - * @param string $haystack - * @param string $needle - * - * @return bool - */ - public static function startsWith($haystack, $needle) - { - return $needle === '' || strrpos($haystack, $needle, -\strlen($haystack)) !== false; - } - /** * @param string $haystack * @param string $needle @@ -28,8 +17,13 @@ final class StringUtil */ public static function endsWith($haystack, $needle) { - return $needle === '' || (($temp = \strlen($haystack) - \strlen($needle)) >= 0 - && strpos($haystack, $needle, $temp) !== false); + $length = \strlen($needle); + + if ($length === 0) { + return true; + } + + return substr($haystack, -$length) === $needle; } /** diff --git a/src/ZipFile.php b/src/ZipFile.php index 1af7828..fc551e8 100644 --- a/src/ZipFile.php +++ b/src/ZipFile.php @@ -570,7 +570,7 @@ class ZipFile implements ZipFileInterface $zipEntry->setCompressionMethod($compressionMethod); $zipEntry->setCreatedOS(ZipPlatform::OS_UNIX); $zipEntry->setExtractedOS(ZipPlatform::OS_UNIX); - $zipEntry->setUnixMode(010644); + $zipEntry->setUnixMode(0100644); $zipEntry->setTime(time()); $this->addZipEntry($zipEntry); @@ -794,7 +794,7 @@ class ZipFile implements ZipFileInterface $zipEntry = new ZipEntry($entryName); if ($fstat !== false) { - $unixMode = (int) sprintf('%o', $fstat['mode']); + $unixMode = $fstat['mode']; $length = $fstat['size']; if ($compressionMethod === null || $compressionMethod === ZipEntry::UNKNOWN) { @@ -812,7 +812,7 @@ class ZipFile implements ZipFileInterface $zipEntry->setUncompressedSize($length); } } else { - $unixMode = 010644; + $unixMode = 0100644; if ($compressionMethod === null || $compressionMethod === ZipEntry::UNKNOWN) { $compressionMethod = ZipCompressionMethod::DEFLATED; diff --git a/tests/SlowTests/Zip64Test.php b/tests/SlowTests/Zip64Test.php index e262413..608137f 100644 --- a/tests/SlowTests/Zip64Test.php +++ b/tests/SlowTests/Zip64Test.php @@ -17,8 +17,6 @@ class Zip64Test extends ZipTestCase { /** * @throws ZipException - * - * @runInSeparateProcess */ public function testCreateLargeZip64File() { diff --git a/tests/Zip64Test.php b/tests/Zip64Test.php index bcb12cb..a87752f 100644 --- a/tests/Zip64Test.php +++ b/tests/Zip64Test.php @@ -22,6 +22,12 @@ class Zip64Test extends ZipTestCase */ public function testOver65535FilesInZip() { + if (\PHP_INT_SIZE === 4) { // php 32 bit + static::markTestSkipped('Only php-64 bit.'); + + return; + } + $countFiles = 0xffff + 1; $zipFile = new ZipFile(); diff --git a/tests/ZipEntryTest.php b/tests/ZipEntryTest.php new file mode 100644 index 0000000..65993c6 --- /dev/null +++ b/tests/ZipEntryTest.php @@ -0,0 +1,1561 @@ +getName(), 'entry'); + static::assertFalse($zipEntry->isDirectory()); + static::assertNull($zipEntry->getData()); + static::assertSame($zipEntry->getCompressionMethod(), ZipEntry::UNKNOWN); + static::assertSame($zipEntry->getCreatedOS(), ZipEntry::UNKNOWN); + static::assertSame($zipEntry->getExtractedOS(), ZipEntry::UNKNOWN); + static::assertSame($zipEntry->getSoftwareVersion(), ZipVersion::v10_DEFAULT_MIN); + static::assertSame($zipEntry->getExtractVersion(), ZipVersion::v10_DEFAULT_MIN); + static::assertSame($zipEntry->getGeneralPurposeBitFlags(), 0); + static::assertSame($zipEntry->getDosTime(), ZipEntry::UNKNOWN); + static::assertSame($zipEntry->getTime(), ZipEntry::UNKNOWN); + static::assertSame($zipEntry->getCrc(), ZipEntry::UNKNOWN); + static::assertSame($zipEntry->getCompressedSize(), ZipEntry::UNKNOWN); + static::assertSame($zipEntry->getUncompressedSize(), ZipEntry::UNKNOWN); + static::assertSame($zipEntry->getInternalAttributes(), 0); + static::assertSame($zipEntry->getExternalAttributes(), DosAttrs::DOS_ARCHIVE); + static::assertSame($zipEntry->getLocalHeaderOffset(), 0); + static::assertInstanceOf(ExtraFieldsCollection::class, $zipEntry->getCdExtraFields()); + static::assertInstanceOf(ExtraFieldsCollection::class, $zipEntry->getLocalExtraFields()); + static::assertCount(0, $zipEntry->getCdExtraFields()); + static::assertCount(0, $zipEntry->getLocalExtraFields()); + static::assertSame($zipEntry->getComment(), ''); + static::assertNull($zipEntry->getPassword()); + static::assertSame($zipEntry->getEncryptionMethod(), ZipEncryptionMethod::NONE); + static::assertSame($zipEntry->getCompressionLevel(), ZipCompressionLevel::NORMAL); + static::assertNull($zipEntry->getCharset()); + static::assertNull($zipEntry->getATime()); + static::assertNull($zipEntry->getCTime()); + static::assertSame($zipEntry->getUnixMode(), 0100644); + + $zipDirEntry = $zipEntry->rename('directory/'); + static::assertNotSame($zipEntry, $zipDirEntry); + static::assertSame($zipDirEntry->getName(), 'directory/'); + static::assertTrue($zipDirEntry->isDirectory()); + static::assertSame($zipDirEntry->getExternalAttributes(), DosAttrs::DOS_DIRECTORY); + static::assertSame($zipDirEntry->getUnixMode(), 040755); + static::assertNotSame($zipDirEntry->getName(), $zipEntry->getName()); + static::assertNotSame($zipDirEntry->isDirectory(), $zipEntry->isDirectory()); + static::assertNotSame($zipDirEntry->getExternalAttributes(), $zipEntry->getExternalAttributes()); + static::assertNotSame($zipDirEntry->getUnixMode(), $zipEntry->getUnixMode()); + } + + /** + * @dataProvider provideEmptyName + * + * @param string|null $entryName + * @param string $exceptionMessage + */ + public function testEmptyName($entryName, $exceptionMessage) + { + $this->setExpectedException(InvalidArgumentException::class, $exceptionMessage); + + new ZipEntry($entryName); + } + + /** + * @return array + */ + public function provideEmptyName() + { + return [ + ['', 'Empty zip entry name'], + ['/', 'Empty zip entry name'], + [null, 'zip entry name is null'], + ]; + } + + /** + * @dataProvider provideEntryName + * + * @param string $entryName + * @param string $actualEntryName + * @param bool $directory + */ + public function testEntryName($entryName, $actualEntryName, $directory) + { + $entry = new ZipEntry($entryName); + static::assertSame($entry->getName(), $actualEntryName); + static::assertSame($entry->isDirectory(), $directory); + } + + /** + * @return array + */ + public function provideEntryName() + { + return [ + ['0', '0', false], + [0, '0', false], + ['directory/', 'directory/', true], + ]; + } + + /** + * @dataProvider provideCompressionMethod + * + * @param int $compressionMethod + * + * @throws ZipUnsupportMethodException + */ + public function testCompressionMethod($compressionMethod) + { + $entry = new ZipEntry('entry'); + static::assertSame($entry->getCompressionMethod(), ZipEntry::UNKNOWN); + + $entry->setCompressionMethod($compressionMethod); + static::assertSame($entry->getCompressionMethod(), $compressionMethod); + } + + /** + * @return array + */ + public function provideCompressionMethod() + { + $provides = [ + [ZipCompressionMethod::STORED], + [ZipCompressionMethod::DEFLATED], + ]; + + if (\extension_loaded('bz2')) { + $provides[] = [ZipCompressionMethod::BZIP2]; + } + + return $provides; + } + + /** + * @dataProvider provideOutOfRangeCompressionMethod + * + * @param int $compressionMethod + * + * @throws ZipUnsupportMethodException + */ + public function testOutOfRangeCompressionMethod($compressionMethod) + { + $this->setExpectedException(InvalidArgumentException::class, 'method out of range: ' . $compressionMethod); + + $zipEntry = new ZipEntry('entry'); + $zipEntry->setCompressionMethod($compressionMethod); + } + + /** + * @return array + */ + public function provideOutOfRangeCompressionMethod() + { + return [ + [-1], + [0x44444], + ]; + } + + /** + * @dataProvider provideUnsupportCompressionMethod + * + * @param int $compressionMethod + * @param string $exceptionMessage + * + * @throws ZipUnsupportMethodException + */ + public function testUnsupportCompressionMethod($compressionMethod, $exceptionMessage) + { + $this->setExpectedException(ZipUnsupportMethodException::class, $exceptionMessage); + + $zipEntry = new ZipEntry('entry'); + $zipEntry->setCompressionMethod($compressionMethod); + } + + /** + * @return array + */ + public function provideUnsupportCompressionMethod() + { + return [ + [1, 'Compression method 1 (Shrunk) is not supported.'], + [2, 'Compression method 2 (Reduced compression factor 1) is not supported.'], + [3, 'Compression method 3 (Reduced compression factor 2) is not supported.'], + [4, 'Compression method 4 (Reduced compression factor 3) is not supported.'], + [5, 'Compression method 5 (Reduced compression factor 4) is not supported.'], + [6, 'Compression method 6 (Imploded) is not supported.'], + [7, 'Compression method 7 (Reserved for Tokenizing compression algorithm) is not supported.'], + [9, 'Compression method 9 (Enhanced Deflating using Deflate64(tm)) is not supported.'], + [10, 'Compression method 10 (PKWARE Data Compression Library Imploding) is not supported.'], + [11, 'Compression method 11 (Reserved by PKWARE) is not supported.'], + [13, 'Compression method 13 (Reserved by PKWARE) is not supported.'], + [14, 'Compression method 14 (LZMA) is not supported.'], + [15, 'Compression method 15 (Reserved by PKWARE) is not supported.'], + [16, 'Compression method 16 (Reserved by PKWARE) is not supported.'], + [17, 'Compression method 17 (Reserved by PKWARE) is not supported.'], + [18, 'Compression method 18 (File is compressed using IBM TERSE (new)) is not supported.'], + [19, 'Compression method 19 (IBM LZ77 z Architecture (PFS)) is not supported.'], + [96, 'Compression method 96 (WinZip JPEG Compression) is not supported.'], + [97, 'Compression method 97 (WavPack compressed data) is not supported.'], + [98, 'Compression method 98 (PPMd version I, Rev 1) is not supported.'], + [ + ZipCompressionMethod::WINZIP_AES, + 'Compression method ' . ZipCompressionMethod::WINZIP_AES . ' (AES Encryption) is not supported.', + ], + [100, 'Compression method 100 (Unknown Method) is not supported.'], + ]; + } + + public function testCharset() + { + $zipEntry = new ZipEntry('entry'); + $zipEntry->setCharset(DosCodePage::CP_CYRILLIC_RUSSIAN); + static::assertSame($zipEntry->getCharset(), DosCodePage::CP_CYRILLIC_RUSSIAN); + + $zipEntry->setCharset(null); + static::assertNull($zipEntry->getCharset()); + } + + public function testEmptyCharset() + { + $this->setExpectedException(InvalidArgumentException::class, 'Empty charset'); + + $zipEntry = new ZipEntry('entry'); + $zipEntry->setCharset(''); + } + + public function testRenameAndDeleteUnicodePath() + { + $entryName = 'файл.txt'; + $charset = DosCodePage::CP_CYRILLIC_RUSSIAN; + $dosEntryName = DosCodePage::fromUTF8($entryName, $charset); + static::assertSame(DosCodePage::toUTF8($dosEntryName, $charset), $entryName); + + $unicodePathExtraField = UnicodePathExtraField::create($entryName); + + $zipEntry = new ZipEntry($dosEntryName, $charset); + static::assertSame($zipEntry->getName(), $dosEntryName); + static::assertSame($zipEntry->getCharset(), $charset); + static::assertFalse($zipEntry->isUtf8Flag()); + $zipEntry->addExtraField($unicodePathExtraField); + static::assertSame( + $zipEntry->getLocalExtraField(UnicodePathExtraField::HEADER_ID), + $unicodePathExtraField + ); + static::assertSame( + $zipEntry->getCdExtraField(UnicodePathExtraField::HEADER_ID), + $unicodePathExtraField + ); + + $utf8EntryName = $zipEntry->rename($entryName); + static::assertSame($utf8EntryName->getName(), $entryName); + static::assertTrue($utf8EntryName->isUtf8Flag()); + static::assertNull($utf8EntryName->getCharset()); + static::assertNull($utf8EntryName->getLocalExtraField(UnicodePathExtraField::HEADER_ID)); + static::assertNull($utf8EntryName->getCdExtraField(UnicodePathExtraField::HEADER_ID)); + } + + public function testData() + { + $zipEntry = new ZipEntry('entry'); + static::assertNull($zipEntry->getData()); + + $zipData = new ZipNewData($zipEntry, 'Text contents'); + + $zipEntry->setData($zipData); + static::assertSame($zipEntry->getData(), $zipData); + + $zipEntry->setData(null); + static::assertNull($zipEntry->getData()); + } + + /** + * @dataProvider providePlatform + * + * @param int $zipOS + */ + public function testCreatedOS($zipOS) + { + $zipEntry = new ZipEntry('entry'); + static::assertSame($zipEntry->getCreatedOS(), ZipEntry::UNKNOWN); + $zipEntry->setCreatedOS($zipOS); + static::assertSame($zipEntry->getCreatedOS(), $zipOS); + } + + /** + * @return array + */ + public function providePlatform() + { + return [ + [ZipPlatform::OS_DOS], + [ZipPlatform::OS_UNIX], + [ZipPlatform::OS_MAC_OSX], + ]; + } + + /** + * @dataProvider providePlatform + * + * @param int $zipOS + */ + public function testExtractedOS($zipOS) + { + $zipEntry = new ZipEntry('entry'); + static::assertSame($zipEntry->getExtractedOS(), ZipEntry::UNKNOWN); + $zipEntry->setExtractedOS($zipOS); + static::assertSame($zipEntry->getExtractedOS(), $zipOS); + } + + /** + * @dataProvider provideInvalidPlatform + * + * @param int $zipOS + */ + public function testInvalidCreatedOs($zipOS) + { + $this->setExpectedException(InvalidArgumentException::class, 'Platform out of range'); + + $zipEntry = new ZipEntry('entry'); + $zipEntry->setCreatedOS($zipOS); + } + + /** + * @return array + */ + public function provideInvalidPlatform() + { + return [ + [-1], + [0xff + 1], + ]; + } + + /** + * @dataProvider provideInvalidPlatform + * + * @param int $zipOS + */ + public function testInvalidExtractedOs($zipOS) + { + $this->setExpectedException(InvalidArgumentException::class, 'Platform out of range'); + + $zipEntry = new ZipEntry('entry'); + $zipEntry->setExtractedOS($zipOS); + } + + /** + * @throws ZipException + */ + public function testAutoExtractVersion() + { + $zipEntry = new ZipEntry('entry'); + static::assertSame($zipEntry->getExtractVersion(), ZipVersion::v10_DEFAULT_MIN); + + $zipEntry->setCompressionMethod(ZipCompressionMethod::DEFLATED); + static::assertSame($zipEntry->getExtractVersion(), ZipVersion::v20_DEFLATED_FOLDER_ZIPCRYPTO); + + static::assertSame( + (new ZipEntry('directory/'))->getExtractVersion(), + ZipVersion::v20_DEFLATED_FOLDER_ZIPCRYPTO + ); + + if (\extension_loaded('bz2')) { + $zipEntry->setCompressionMethod(ZipCompressionMethod::BZIP2); + static::assertSame($zipEntry->getExtractVersion(), ZipVersion::v46_BZIP2); + } + + $zipEntry->setCompressionMethod(ZipCompressionMethod::STORED); + static::assertSame($zipEntry->getExtractVersion(), ZipVersion::v10_DEFAULT_MIN); + + $zipEntry->setPassword('12345', ZipEncryptionMethod::PKWARE); + static::assertSame($zipEntry->getExtractVersion(), ZipVersion::v20_DEFLATED_FOLDER_ZIPCRYPTO); + + $zipEntry->setEncryptionMethod(ZipEncryptionMethod::WINZIP_AES_256); + static::assertSame($zipEntry->getExtractVersion(), ZipVersion::v51_ENCR_AES_RC2_CORRECT); + } + + /** + * @throws ZipException + */ + public function testExtractVersion() + { + $zipEntry = new ZipEntry('entry'); + static::assertSame($zipEntry->getExtractVersion(), ZipVersion::v10_DEFAULT_MIN); + + $zipEntry->setExtractVersion(ZipVersion::v63_LZMA_PPMD_BLOWFISH_TWOFISH); + static::assertSame($zipEntry->getExtractVersion(), ZipVersion::v63_LZMA_PPMD_BLOWFISH_TWOFISH); + + $renameEntry = $zipEntry->rename('new_entry'); + static::assertSame($renameEntry->getExtractVersion(), ZipVersion::v63_LZMA_PPMD_BLOWFISH_TWOFISH); + + $renameDirEntry = $zipEntry->rename('new_directory/'); + static::assertSame($renameDirEntry->getExtractVersion(), ZipVersion::v63_LZMA_PPMD_BLOWFISH_TWOFISH); + + $zipEntry->setExtractVersion(ZipVersion::v10_DEFAULT_MIN); + static::assertSame($zipEntry->getExtractVersion(), ZipVersion::v10_DEFAULT_MIN); + + $renameDirEntry = $zipEntry->rename('new_directory/'); + static::assertSame($renameDirEntry->getExtractVersion(), ZipVersion::v20_DEFLATED_FOLDER_ZIPCRYPTO); + + $zipEntry->setCompressionMethod(ZipCompressionMethod::DEFLATED); + static::assertSame($zipEntry->getExtractVersion(), ZipVersion::v20_DEFLATED_FOLDER_ZIPCRYPTO); + + if (\extension_loaded('bz2')) { + $zipEntry->setExtractVersion(ZipVersion::v10_DEFAULT_MIN); + $zipEntry->setCompressionMethod(ZipCompressionMethod::BZIP2); + static::assertSame($zipEntry->getExtractVersion(), ZipVersion::v46_BZIP2); + } + + $zipEntry->setExtractVersion(ZipVersion::v63_LZMA_PPMD_BLOWFISH_TWOFISH); + $zipEntry->setCompressionMethod(ZipCompressionMethod::STORED); + static::assertSame($zipEntry->getExtractVersion(), ZipVersion::v10_DEFAULT_MIN); + + $zipEntry->setExtractVersion(ZipVersion::v10_DEFAULT_MIN); + $zipEntry->setPassword('12345', ZipEncryptionMethod::PKWARE); + static::assertSame($zipEntry->getExtractVersion(), ZipVersion::v20_DEFLATED_FOLDER_ZIPCRYPTO); + + $zipEntry->setExtractVersion(ZipVersion::v10_DEFAULT_MIN); + $zipEntry->setEncryptionMethod(ZipEncryptionMethod::WINZIP_AES_256); + static::assertSame($zipEntry->getExtractVersion(), ZipVersion::v51_ENCR_AES_RC2_CORRECT); + } + + public function testSoftwareVersion() + { + $zipEntry = new ZipEntry('entry'); + static::assertSame($zipEntry->getSoftwareVersion(), $zipEntry->getExtractVersion()); + + $zipEntry->setExtractVersion(ZipVersion::v45_ZIP64_EXT); + static::assertSame($zipEntry->getSoftwareVersion(), $zipEntry->getExtractVersion()); + + $softwareVersion = 35; + $zipEntry->setSoftwareVersion($softwareVersion); + static::assertSame($softwareVersion, $zipEntry->getSoftwareVersion()); + static::assertSame($zipEntry->getExtractVersion(), ZipVersion::v45_ZIP64_EXT); + + $zipEntry->setExtractVersion(ZipVersion::v63_LZMA_PPMD_BLOWFISH_TWOFISH); + static::assertNotSame($zipEntry->getSoftwareVersion(), $zipEntry->getExtractVersion()); + static::assertSame($softwareVersion, $zipEntry->getSoftwareVersion()); + static::assertSame($zipEntry->getExtractVersion(), ZipVersion::v63_LZMA_PPMD_BLOWFISH_TWOFISH); + } + + public function testSize() + { + $zipEntry = new ZipEntry('entry'); + static::assertSame($zipEntry->getCompressedSize(), ZipEntry::UNKNOWN); + static::assertSame($zipEntry->getUncompressedSize(), ZipEntry::UNKNOWN); + + $compressedSize = 100000; + $uncompressedSize = 400000; + + $zipEntry->setCompressedSize($compressedSize); + $zipEntry->setUncompressedSize($uncompressedSize); + static::assertSame($zipEntry->getCompressedSize(), $compressedSize); + static::assertSame($zipEntry->getUncompressedSize(), $uncompressedSize); + + $zipEntry->setCompressedSize(ZipEntry::UNKNOWN); + $zipEntry->setUncompressedSize(ZipEntry::UNKNOWN); + static::assertSame($zipEntry->getCompressedSize(), ZipEntry::UNKNOWN); + static::assertSame($zipEntry->getUncompressedSize(), ZipEntry::UNKNOWN); + } + + public function testInvalidCompressedSize() + { + $this->setExpectedException(InvalidArgumentException::class, 'Compressed size < -1'); + + $zipEntry = new ZipEntry('entry'); + $zipEntry->setCompressedSize(-2); + } + + public function testInvalidUncompressedSize() + { + $this->setExpectedException(InvalidArgumentException::class, 'Uncompressed size < -1'); + + $zipEntry = new ZipEntry('entry'); + $zipEntry->setUncompressedSize(-2); + } + + public function testLocalHeaderOffset() + { + $zipEntry = new ZipEntry('entry'); + static::assertSame($zipEntry->getLocalHeaderOffset(), 0); + + $localHeaderOffset = 10000; + $zipEntry->setLocalHeaderOffset($localHeaderOffset); + static::assertSame($zipEntry->getLocalHeaderOffset(), $localHeaderOffset); + + $this->setExpectedException(InvalidArgumentException::class, 'Negative $localHeaderOffset'); + $zipEntry->setLocalHeaderOffset(-1); + } + + public function testGeneralPurposeBitFlags() + { + $zipEntry = new ZipEntry('entry'); + static::assertSame($zipEntry->getGeneralPurposeBitFlags(), 0); + static::assertFalse($zipEntry->isUtf8Flag()); + static::assertFalse($zipEntry->isEncrypted()); + static::assertFalse($zipEntry->isStrongEncryption()); + static::assertFalse($zipEntry->isDataDescriptorEnabled()); + + $gpbf = GeneralPurposeBitFlag::DATA_DESCRIPTOR | GeneralPurposeBitFlag::UTF8; + $zipEntry->setGeneralPurposeBitFlags($gpbf); + static::assertSame($zipEntry->getGeneralPurposeBitFlags(), $gpbf); + static::assertTrue($zipEntry->isDataDescriptorEnabled()); + static::assertTrue($zipEntry->isUtf8Flag()); + + $zipEntry->setGeneralPurposeBitFlags(0); + static::assertSame($zipEntry->getGeneralPurposeBitFlags(), 0); + static::assertFalse($zipEntry->isUtf8Flag()); + static::assertFalse($zipEntry->isDataDescriptorEnabled()); + + $zipEntry->enableUtf8Name(true); + static::assertTrue($zipEntry->isUtf8Flag()); + static::assertSame( + ($zipEntry->getGeneralPurposeBitFlags() & GeneralPurposeBitFlag::UTF8), + GeneralPurposeBitFlag::UTF8 + ); + $zipEntry->enableUtf8Name(false); + static::assertFalse($zipEntry->isUtf8Flag()); + static::assertSame( + ($zipEntry->getGeneralPurposeBitFlags() & GeneralPurposeBitFlag::UTF8), + 0 + ); + + $zipEntry->enableDataDescriptor(true); + static::assertTrue($zipEntry->isDataDescriptorEnabled()); + static::assertSame( + ($zipEntry->getGeneralPurposeBitFlags() & GeneralPurposeBitFlag::DATA_DESCRIPTOR), + GeneralPurposeBitFlag::DATA_DESCRIPTOR + ); + $zipEntry->enableDataDescriptor(false); + static::assertFalse($zipEntry->isDataDescriptorEnabled()); + static::assertSame( + ($zipEntry->getGeneralPurposeBitFlags() & GeneralPurposeBitFlag::DATA_DESCRIPTOR), + 0 + ); + } + + public function testEncryptionGPBF() + { + $zipEntry = new ZipEntry('entry'); + static::assertFalse($zipEntry->isEncrypted()); + + $zipEntry->setGeneralPurposeBitFlags(GeneralPurposeBitFlag::ENCRYPTION); + + static::assertSame( + ($zipEntry->getGeneralPurposeBitFlags() & GeneralPurposeBitFlag::ENCRYPTION), + GeneralPurposeBitFlag::ENCRYPTION + ); + static::assertTrue($zipEntry->isEncrypted()); + + $zipEntry->disableEncryption(); + static::assertSame( + ($zipEntry->getGeneralPurposeBitFlags() & GeneralPurposeBitFlag::ENCRYPTION), + 0 + ); + static::assertFalse($zipEntry->isEncrypted()); + + // SIC! Strong encryption is not supported in ZipReader and ZipWriter + static::assertFalse($zipEntry->isStrongEncryption()); + $zipEntry->setGeneralPurposeBitFlags(GeneralPurposeBitFlag::STRONG_ENCRYPTION); + static::assertTrue($zipEntry->isStrongEncryption()); + } + + /** + * @dataProvider provideInvalidGPBF + * + * @param int $gpbf + */ + public function testInvalidGPBF($gpbf) + { + $this->setExpectedException(InvalidArgumentException::class, 'general purpose bit flags out of range'); + + $zipEntry = new ZipEntry('entry'); + $zipEntry->setGeneralPurposeBitFlags($gpbf); + } + + /** + * @return array + */ + public function provideInvalidGPBF() + { + return [ + [-1], + [0x10000], + ]; + } + + /** + * @dataProvider provideCompressionLevelGPBF + * + * @param int $compressionLevel + * @param bool $bit1 + * @param bool $bit2 + * + * @throws ZipUnsupportMethodException + */ + public function testSetCompressionFlags($compressionLevel, $bit1, $bit2) + { + $zipEntry = new ZipEntry('entry'); + $zipEntry->setCompressionMethod(ZipCompressionMethod::DEFLATED); + + $gpbf = ($bit1 ? GeneralPurposeBitFlag::COMPRESSION_FLAG1 : 0) | + ($bit2 ? GeneralPurposeBitFlag::COMPRESSION_FLAG2 : 0); + $zipEntry->setGeneralPurposeBitFlags($gpbf); + static::assertSame($zipEntry->getCompressionLevel(), $compressionLevel); + + static::assertSame( + ( + $zipEntry->getGeneralPurposeBitFlags() & GeneralPurposeBitFlag::COMPRESSION_FLAG1 + ) === GeneralPurposeBitFlag::COMPRESSION_FLAG1, + $bit1, + 'Compression flag1 is not same' + ); + static::assertSame( + ( + $zipEntry->getGeneralPurposeBitFlags() & GeneralPurposeBitFlag::COMPRESSION_FLAG2 + ) === GeneralPurposeBitFlag::COMPRESSION_FLAG2, + $bit2, + 'Compression flag2 is not same' + ); + } + + /** + * @return array + */ + public function provideCompressionLevelGPBF() + { + return [ + [ZipCompressionLevel::SUPER_FAST, true, true], + [ZipCompressionLevel::FAST, false, true], + [ZipCompressionLevel::NORMAL, false, false], + [ZipCompressionLevel::MAXIMUM, true, false], + ]; + } + + /** + * @dataProvider provideCompressionLevels + * + * @param int $compressionLevel + * @param bool $bit1 + * @param bool $bit2 + * + * @throws ZipUnsupportMethodException + */ + public function testSetCompressionLevel($compressionLevel, $bit1, $bit2) + { + $zipEntry = new ZipEntry('entry'); + $zipEntry->setCompressionMethod(ZipCompressionMethod::DEFLATED); + + $zipEntry->setCompressionLevel($compressionLevel); + static::assertSame($zipEntry->getCompressionLevel(), $compressionLevel); + + static::assertSame( + ( + $zipEntry->getGeneralPurposeBitFlags() & GeneralPurposeBitFlag::COMPRESSION_FLAG1 + ) === GeneralPurposeBitFlag::COMPRESSION_FLAG1, + $bit1, + 'Compression flag1 is not same' + ); + static::assertSame( + ( + $zipEntry->getGeneralPurposeBitFlags() & GeneralPurposeBitFlag::COMPRESSION_FLAG2 + ) === GeneralPurposeBitFlag::COMPRESSION_FLAG2, + $bit2, + 'Compression flag2 is not same' + ); + } + + /** + * @return array + */ + public function provideCompressionLevels() + { + return [ + [ZipCompressionLevel::SUPER_FAST, true, true], + [ZipCompressionLevel::FAST, false, true], + [3, false, false], + [4, false, false], + [ZipCompressionLevel::NORMAL, false, false], + [6, false, false], + [7, false, false], + [8, false, false], + [ZipCompressionLevel::MAXIMUM, true, false], + ]; + } + + /** + * @throws ZipException + */ + public function testLegacyDefaultCompressionLevel() + { + $zipEntry = new ZipEntry('entry'); + $zipEntry->setCompressionMethod(ZipCompressionMethod::DEFLATED); + $zipEntry->setCompressionLevel(ZipCompressionLevel::MAXIMUM); + static::assertSame($zipEntry->getCompressionLevel(), ZipCompressionLevel::MAXIMUM); + + $zipEntry->setCompressionLevel(ZipEntry::UNKNOWN); + static::assertSame($zipEntry->getCompressionLevel(), ZipCompressionLevel::NORMAL); + } + + /** + * @dataProvider provideInvalidCompressionLevel + * + * @param int $compressionLevel + * + * @throws ZipException + */ + public function testInvalidCompressionLevel($compressionLevel) + { + $this->setExpectedException( + InvalidArgumentException::class, + 'Invalid compression level. Minimum level ' . ZipCompressionLevel::LEVEL_MIN . + '. Maximum level ' . ZipCompressionLevel::LEVEL_MAX + ); + + $zipEntry = new ZipEntry('entry'); + $zipEntry->setCompressionMethod(ZipCompressionMethod::DEFLATED); + $zipEntry->setCompressionLevel($compressionLevel); + } + + /** + * @return array + */ + public function provideInvalidCompressionLevel() + { + return [ + [0], + [-2], + [10], + [100], + ]; + } + + /** + * @dataProvider provideDosTime + * + * @param int $dosTime + * @param int $timestamp + */ + public function testDosTime($dosTime, $timestamp) + { + $zipEntry = new ZipEntry('entry'); + static::assertSame($zipEntry->getDosTime(), ZipEntry::UNKNOWN); + + $zipEntry->setDosTime($dosTime); + static::assertSame($zipEntry->getDosTime(), $dosTime); + static::assertSame($zipEntry->getTime(), $timestamp); + + $zipEntry->setTime($timestamp); + static::assertSame($zipEntry->getTime(), $timestamp); + static::assertSame($zipEntry->getDosTime(), $dosTime); + } + + /** + * @return array + */ + public function provideDosTime() + { + return [ + [0, 312757200], + [1043487716, 1295339468], + [1177556759, 1421366206], + [1282576076, 1521384864], + ]; + } + + /** + * @dataProvider provideInvalidDosTime + * + * @param int $dosTime + */ + public function testInvalidDosTime($dosTime) + { + $this->setExpectedException(InvalidArgumentException::class, 'DosTime out of range'); + + $zipEntry = new ZipEntry('entry'); + $zipEntry->setDosTime($dosTime); + } + + /** + * @return array + */ + public function provideInvalidDosTime() + { + return [ + [-1], + [0xffffffff + 1], + ]; + } + + public function testSetTime() + { + $zipEntry = new ZipEntry('entry'); + static::assertSame($zipEntry->getDosTime(), ZipEntry::UNKNOWN); + $zipEntry->setTime(ZipEntry::UNKNOWN); + static::assertSame($zipEntry->getDosTime(), 0); + + $zipEntry->setTime(0); + static::assertSame($zipEntry->getDosTime(), 0); + } + + /** + * @dataProvider provideExternalAttributes + * + * @param string $entryName + * @param int $expectedExternalAttr + * @param int $createdOS + * @param int $extractedOS + * @param int|null $externalAttr + * @param int $unixMode + * + * @noinspection PhpTooManyParametersInspection + */ + public function testExternalAttributes( + $entryName, + $expectedExternalAttr, + $createdOS, + $extractedOS, + $externalAttr, + $unixMode + ) { + $zipEntry = new ZipEntry($entryName); + static::assertSame($zipEntry->getExternalAttributes(), $expectedExternalAttr); + $zipEntry + ->setCreatedOS($createdOS) + ->setExtractedOS($extractedOS) + ; + + if ($externalAttr !== null) { + $zipEntry->setExternalAttributes($externalAttr); + static::assertSame($zipEntry->getExternalAttributes(), $externalAttr); + } + + static::assertSame($zipEntry->getUnixMode(), $unixMode); + } + + /** + * @return array + */ + public function provideExternalAttributes() + { + return [ + [ + 'entry.txt', + DosAttrs::DOS_ARCHIVE, + ZipPlatform::OS_UNIX, + ZipPlatform::OS_UNIX, + (010644 << 16) | DosAttrs::DOS_ARCHIVE, + 010644, + ], + [ + 'dir/', + DosAttrs::DOS_DIRECTORY, + ZipPlatform::OS_UNIX, + ZipPlatform::OS_UNIX, + (040755 << 16) | DosAttrs::DOS_DIRECTORY, + 040755, + ], + [ + 'entry.txt', + DosAttrs::DOS_ARCHIVE, + ZipPlatform::OS_DOS, + ZipPlatform::OS_DOS, + null, + 0100644, + ], + [ + 'entry.txt', + DosAttrs::DOS_ARCHIVE, + ZipPlatform::OS_DOS, + ZipPlatform::OS_UNIX, + null, + 0100644, + ], + [ + 'entry.txt', + DosAttrs::DOS_ARCHIVE, + ZipPlatform::OS_UNIX, + ZipPlatform::OS_DOS, + null, + 0100644, + ], + [ + 'dir/', + DosAttrs::DOS_DIRECTORY, + ZipPlatform::OS_DOS, + ZipPlatform::OS_DOS, + null, + 040755, + ], + [ + 'dir/', + DosAttrs::DOS_DIRECTORY, + ZipPlatform::OS_DOS, + ZipPlatform::OS_UNIX, + null, + 040755, + ], + [ + 'dir/', + DosAttrs::DOS_DIRECTORY, + ZipPlatform::OS_UNIX, + ZipPlatform::OS_DOS, + null, + 040755, + ], + [ + 'entry.txt', + DosAttrs::DOS_ARCHIVE, + ZipPlatform::OS_UNIX, + ZipPlatform::OS_UNIX, + 0777 << 16, + 0777, + ], + ]; + } + + /** + * @dataProvider provideInvalidExternalAttributes + * + * @param int $externalAttributes + */ + public function testInvalidExternalAttributes($externalAttributes) + { + $this->setExpectedException(InvalidArgumentException::class, 'external attributes out of range'); + + $zipEntry = new ZipEntry('entry'); + $zipEntry->setExternalAttributes($externalAttributes); + } + + /** + * @return array + */ + public function provideInvalidExternalAttributes() + { + return [ + [-1], + [0xffffffff + 1], + ]; + } + + public function testInternalAttributes() + { + $zipEntry = new ZipEntry('entry'); + static::assertSame($zipEntry->getInternalAttributes(), 0); + + $zipEntry->setInternalAttributes(1); + static::assertSame($zipEntry->getInternalAttributes(), 1); + } + + /** + * @dataProvider provideInvalidInternalAttributes + * + * @param int $internalAttributes + */ + public function testInvalidInternalAttributes($internalAttributes) + { + $this->setExpectedException(InvalidArgumentException::class, 'internal attributes out of range'); + + $zipEntry = new ZipEntry('entry'); + $zipEntry->setInternalAttributes($internalAttributes); + } + + /** + * @return array + */ + public function provideInvalidInternalAttributes() + { + return [ + [-1], + [0xffff + 1], + ]; + } + + public function testExtraFields() + { + $zipEntry = new ZipEntry('entry'); + + static::assertInstanceOf(ExtraFieldsCollection::class, $zipEntry->getCdExtraFields()); + static::assertInstanceOf(ExtraFieldsCollection::class, $zipEntry->getLocalExtraFields()); + static::assertCount(0, $zipEntry->getCdExtraFields()); + static::assertCount(0, $zipEntry->getLocalExtraFields()); + + $extraNtfs = new NtfsExtraField(time(), time() - 10000, time() - 100000); + $extraAsi = new AsiExtraField(010644); + $extraJar = new JarMarkerExtraField(); + + $zipEntry->getLocalExtraFields()->add($extraNtfs); + $zipEntry->getCdExtraFields()->add($extraNtfs); + static::assertCount(1, $zipEntry->getCdExtraFields()); + static::assertCount(1, $zipEntry->getLocalExtraFields()); + + $zipEntry->addExtraField($extraAsi); + static::assertCount(2, $zipEntry->getCdExtraFields()); + static::assertCount(2, $zipEntry->getLocalExtraFields()); + + $zipEntry->addCdExtraField($extraJar); + static::assertCount(3, $zipEntry->getCdExtraFields()); + static::assertCount(2, $zipEntry->getLocalExtraFields()); + + static::assertSame($zipEntry->getCdExtraField(JarMarkerExtraField::HEADER_ID), $extraJar); + static::assertNull($zipEntry->getLocalExtraField(JarMarkerExtraField::HEADER_ID)); + static::assertSame($zipEntry->getLocalExtraField(AsiExtraField::HEADER_ID), $extraAsi); + + static::assertSame( + [$extraNtfs, $extraAsi, $extraJar], + array_values($zipEntry->getCdExtraFields()->getAll()) + ); + static::assertSame( + [$extraNtfs, $extraAsi], + array_values($zipEntry->getLocalExtraFields()->getAll()) + ); + + $zipEntry->removeExtraField(AsiExtraField::HEADER_ID); + static::assertNull($zipEntry->getCdExtraField(AsiExtraField::HEADER_ID)); + static::assertNull($zipEntry->getLocalExtraField(AsiExtraField::HEADER_ID)); + + static::assertCount(2, $zipEntry->getCdExtraFields()); + static::assertCount(1, $zipEntry->getLocalExtraFields()); + static::assertSame( + [$extraNtfs, $extraJar], + array_values($zipEntry->getCdExtraFields()->getAll()) + ); + static::assertSame( + [$extraNtfs], + array_values($zipEntry->getLocalExtraFields()->getAll()) + ); + + static::assertTrue($zipEntry->hasExtraField(NtfsExtraField::HEADER_ID)); + static::assertTrue($zipEntry->hasExtraField(JarMarkerExtraField::HEADER_ID)); + static::assertFalse($zipEntry->hasExtraField(AsiExtraField::HEADER_ID)); + } + + public function testComment() + { + $zipEntry = new ZipEntry('entry'); + static::assertSame($zipEntry->getComment(), ''); + $zipEntry->setComment('comment'); + static::assertSame($zipEntry->getComment(), 'comment'); + $zipEntry->setComment(null); + static::assertSame($zipEntry->getComment(), ''); + static::assertFalse($zipEntry->isUtf8Flag()); + $zipEntry->setComment('комментарий'); + static::assertTrue($zipEntry->isUtf8Flag()); + static::assertSame($zipEntry->getComment(), 'комментарий'); + } + + /** + * @throws \Exception + */ + public function testLongComment() + { + $this->setExpectedException(InvalidArgumentException::class, 'Comment too long'); + + $longComment = random_bytes(0xffff + 1); + $zipEntry = new ZipEntry('entry'); + $zipEntry->setComment($longComment); + } + + /** + * @dataProvider provideDataDescriptorRequired + * + * @param int $crc + * @param int $compressedSize + * @param int $uncompressedSize + * @param bool $required + */ + public function testDataDescriptorRequired($crc, $compressedSize, $uncompressedSize, $required) + { + $zipEntry = new ZipEntry('entry'); + $zipEntry->setCrc($crc); + $zipEntry->setCompressedSize($compressedSize); + $zipEntry->setUncompressedSize($uncompressedSize); + + static::assertSame($zipEntry->isDataDescriptorRequired(), $required); + static::assertSame($zipEntry->getCrc(), $crc); + static::assertSame($zipEntry->getCompressedSize(), $compressedSize); + static::assertSame($zipEntry->getUncompressedSize(), $uncompressedSize); + } + + /** + * @return array + */ + public function provideDataDescriptorRequired() + { + return [ + [ZipEntry::UNKNOWN, ZipEntry::UNKNOWN, ZipEntry::UNKNOWN, true], + [0xF33F33, ZipEntry::UNKNOWN, ZipEntry::UNKNOWN, true], + [0xF33F33, 11111111, ZipEntry::UNKNOWN, true], + [0xF33F33, ZipEntry::UNKNOWN, 22333333, true], + [ZipEntry::UNKNOWN, 11111111, ZipEntry::UNKNOWN, true], + [ZipEntry::UNKNOWN, 11111111, 22333333, true], + [ZipEntry::UNKNOWN, ZipEntry::UNKNOWN, 22333333, true], + [0xF33F33, 11111111, 22333333, false], + ]; + } + + /** + * @dataProvider provideEncryption + * + * @param string|null $password + * @param int|null $encryptionMethod + * @param bool $encrypted + * @param int $expectedEncryptionMethod + */ + public function testEncryption($password, $encryptionMethod, $encrypted, $expectedEncryptionMethod) + { + $zipEntry = new ZipEntry('entry'); + $zipEntry->setPassword($password, $encryptionMethod); + + static::assertSame($zipEntry->isEncrypted(), $encrypted); + static::assertSame($zipEntry->getPassword(), $password); + static::assertSame($zipEntry->getEncryptionMethod(), $expectedEncryptionMethod); + + $zipEntry->setPassword($password, null); + static::assertSame($zipEntry->getEncryptionMethod(), $expectedEncryptionMethod); + } + + /** + * @return array + */ + public function provideEncryption() + { + return [ + [null, null, false, ZipEncryptionMethod::NONE], + [null, ZipEncryptionMethod::WINZIP_AES_256, false, ZipEncryptionMethod::NONE], + ['12345', null, true, ZipEncryptionMethod::WINZIP_AES_256], + ['12345', ZipEncryptionMethod::PKWARE, true, ZipEncryptionMethod::PKWARE], + ['12345', ZipEncryptionMethod::WINZIP_AES_256, true, ZipEncryptionMethod::WINZIP_AES_256], + ['12345', ZipEncryptionMethod::WINZIP_AES_128, true, ZipEncryptionMethod::WINZIP_AES_128], + ['12345', ZipEncryptionMethod::WINZIP_AES_192, true, ZipEncryptionMethod::WINZIP_AES_192], + ]; + } + + public function testDirectoryEncryption() + { + $zipEntry = new ZipEntry('directory/'); + $zipEntry->setPassword('12345', ZipEncryptionMethod::WINZIP_AES_256); + static::assertTrue($zipEntry->isDirectory()); + static::assertNull($zipEntry->getPassword()); + static::assertFalse($zipEntry->isEncrypted()); + static::assertSame($zipEntry->getEncryptionMethod(), ZipEncryptionMethod::NONE); + } + + /** + * @dataProvider provideEncryptionMethod + * + * @param int|null $encryptionMethod + * @param int $expectedEncryptionMethod + * @param bool $encrypted + * @param int $extractVersion + */ + public function testEncryptionMethod( + $encryptionMethod, + $expectedEncryptionMethod, + $encrypted, + $extractVersion + ) { + $zipEntry = new ZipEntry('entry'); + $zipEntry->setEncryptionMethod($encryptionMethod); + static::assertSame($zipEntry->isEncrypted(), $encrypted); + static::assertSame($zipEntry->getEncryptionMethod(), $expectedEncryptionMethod); + static::assertSame($zipEntry->getExtractVersion(), $extractVersion); + } + + /** + * @return array + */ + public function provideEncryptionMethod() + { + return [ + [ + null, + ZipEncryptionMethod::NONE, + false, + ZipVersion::v10_DEFAULT_MIN, + ], + [ + ZipEncryptionMethod::NONE, + ZipEncryptionMethod::NONE, + false, + ZipVersion::v10_DEFAULT_MIN, + ], + [ + ZipEncryptionMethod::PKWARE, + ZipEncryptionMethod::PKWARE, + true, + ZipVersion::v20_DEFLATED_FOLDER_ZIPCRYPTO, + ], + [ + ZipEncryptionMethod::WINZIP_AES_256, + ZipEncryptionMethod::WINZIP_AES_256, + true, + ZipVersion::v51_ENCR_AES_RC2_CORRECT, + ], + [ + ZipEncryptionMethod::WINZIP_AES_192, + ZipEncryptionMethod::WINZIP_AES_192, + true, + ZipVersion::v51_ENCR_AES_RC2_CORRECT, + ], + [ + ZipEncryptionMethod::WINZIP_AES_128, + ZipEncryptionMethod::WINZIP_AES_128, + true, + ZipVersion::v51_ENCR_AES_RC2_CORRECT, + ], + ]; + } + + /** + * @dataProvider provideInvalidEncryptionMethod + * + * @param int $encryptionMethod + */ + public function testInvalidEncryptionMethod($encryptionMethod) + { + $this->setExpectedException( + InvalidArgumentException::class, + 'Encryption method ' . $encryptionMethod . ' is not supported.' + ); + + $zipEntry = new ZipEntry('entry'); + $zipEntry->setEncryptionMethod($encryptionMethod); + } + + /** + * @return array + */ + public function provideInvalidEncryptionMethod() + { + return [ + [-2], + [4], + [5], + ]; + } + + /** + * @dataProvider provideUnixMode + * + * @param string $entryName + * @param int $unixMode + */ + public function testUnixMode($entryName, $unixMode) + { + $zipEntry = new ZipEntry($entryName); + $zipEntry->setUnixMode($unixMode); + + static::assertSame($zipEntry->getUnixMode(), $unixMode); + static::assertSame($zipEntry->getCreatedOS(), ZipPlatform::OS_UNIX); + } + + /** + * @return array + */ + public function provideUnixMode() + { + return [ + ['entry.txt', 0700], // read, write, & execute only for owner + ['entry.txt', 0770], // read, write, & execute for owner and group + ['entry.txt', 0777], // read, write, & execute for owner, group and others + ['entry.txt', 0111], // execute + ['entry.txt', 0222], // write + ['entry.txt', 0333], // write & execute + ['entry.txt', 0444], // read + ['entry.txt', 0555], // read & execute + ['entry.txt', 0666], // read & write + ['entry.txt', 0740], // owner can read, write, & execute; group can only read; others have no permissions + ['entry.txt', 0777], // owner can read, write, & execute + ['directory/', 040700], // directory, read, write, & execute only for owner + ['directory/', 040770], // directory, read, write, & execute for owner and group + ['directory/', 040777], // directory, read, write, & execute + ]; + } + + /** + * @dataProvider provideUnixMode + * @dataProvider provideSymlink + * + * @param $entryName + * @param $unixMode + * @param bool $symlink + */ + public function testSymlink($entryName, $unixMode, $symlink = false) + { + $zipEntry = new ZipEntry($entryName); + $zipEntry->setUnixMode($unixMode); + static::assertSame($zipEntry->isUnixSymlink(), $symlink); + } + + public function testAsiUnixMode() + { + $unixMode = 0100666; + $asiUnixMode = 0100600; + + $asiExtraField = new AsiExtraField($asiUnixMode); + + $zipEntry = new ZipEntry('entry'); + $zipEntry->setCreatedOS(ZipPlatform::OS_DOS); + $zipEntry->setExtractedOS(ZipPlatform::OS_DOS); + $zipEntry->setExternalAttributes(DosAttrs::DOS_ARCHIVE); + $zipEntry->addExtraField($asiExtraField); + + static::assertSame($zipEntry->getUnixMode(), $asiUnixMode); + + $zipEntry->setUnixMode($unixMode); + static::assertSame($zipEntry->getCreatedOS(), ZipPlatform::OS_UNIX); + static::assertSame($zipEntry->getUnixMode(), $unixMode); + } + + /** + * @return array + */ + public function provideSymlink() + { + return [ + ['entry', 0120644, true], + ['dir/', 0120755, true], + ]; + } + + /** + * @dataProvider provideIsZip64ExtensionsRequired + * + * @param int $compressionSize + * @param int $uncompressionSize + * @param bool $required + */ + public function testIsZip64ExtensionsRequired($compressionSize, $uncompressionSize, $required) + { + if (\PHP_INT_SIZE === 4) { + static::markTestSkipped('only php 64-bit'); + + return; + } + + $zipEntry = new ZipEntry('entry'); + $zipEntry->setCompressedSize($compressionSize); + $zipEntry->setUncompressedSize($uncompressionSize); + static::assertSame($zipEntry->isZip64ExtensionsRequired(), $required); + } + + /** + * @return array + */ + public function provideIsZip64ExtensionsRequired() + { + return [ + [11111111, 22222222, false], + [ZipEntry::UNKNOWN, ZipEntry::UNKNOWN, false], + [ZipEntry::UNKNOWN, ZipConstants::ZIP64_MAGIC + 1, true], + [ZipConstants::ZIP64_MAGIC + 1, ZipEntry::UNKNOWN, true], + [ZipConstants::ZIP64_MAGIC + 1, ZipConstants::ZIP64_MAGIC + 1, true], + [ZipConstants::ZIP64_MAGIC, ZipConstants::ZIP64_MAGIC, false], + [ZipConstants::ZIP64_MAGIC, ZipEntry::UNKNOWN, false], + [ZipEntry::UNKNOWN, ZipConstants::ZIP64_MAGIC, false], + ]; + } + + /** + * @dataProvider provideExtraTime + * + * @param ExtraFieldsCollection $extraFieldsCollection + * @param \DateTimeInterface $mtime + * @param \DateTimeInterface|null $atime + * @param \DateTimeInterface|null $ctime + */ + public function testMTimeATimeCTime(ExtraFieldsCollection $extraFieldsCollection, $mtime, $atime, $ctime) + { + $unixTimestamp = time(); + + $zipEntry = new ZipEntry('entry'); + $zipEntry->setTime($unixTimestamp); + + // converting from unixtime to dos may occur with a small margin of error + static::assertLessThanOrEqual($zipEntry->getMTime()->getTimestamp() + 1, $unixTimestamp); + static::assertGreaterThanOrEqual($zipEntry->getMTime()->getTimestamp() - 1, $unixTimestamp); + + static::assertNull($zipEntry->getATime()); + static::assertNull($zipEntry->getCTime()); + + $zipEntry->getCdExtraFields()->addCollection($extraFieldsCollection); + $zipEntry->getLocalExtraFields()->addCollection($extraFieldsCollection); + + static::assertNotNull($zipEntry->getMTime()); + + static::assertSame($zipEntry->getMTime()->getTimestamp(), $mtime->getTimestamp()); + + if ($atime !== null) { + static::assertSame($zipEntry->getATime()->getTimestamp(), $atime->getTimestamp()); + } else { + static::assertNull($zipEntry->getATime()); + } + + if ($ctime !== null) { + static::assertSame($zipEntry->getCTime()->getTimestamp(), $ctime->getTimestamp()); + } else { + static::assertNull($zipEntry->getCTime()); + } + } + + /** + * @throws \Exception + * + * @return array + */ + public function provideExtraTime() + { + $ntfsExtra = NtfsExtraField::create( + new \DateTimeImmutable('-1 week'), + new \DateTimeImmutable('-1 month'), + new \DateTimeImmutable('-1 year') + ); + + $extendedTimestampExtraField = ExtendedTimestampExtraField::create( + strtotime('-2 weeks'), + strtotime('-2 months'), + strtotime('-2 years') + ); + + $oldUnixExtraField = new OldUnixExtraField( + strtotime('-3 weeks'), + strtotime('-3 months'), + 1000, + 1000 + ); + + $ntfsTimeCollection = new ExtraFieldsCollection(); + $ntfsTimeCollection->add($ntfsExtra); + + $extendedTimestampCollection = new ExtraFieldsCollection(); + $extendedTimestampCollection->add($extendedTimestampExtraField); + + $oldUnixExtraFieldCollection = new ExtraFieldsCollection(); + $oldUnixExtraFieldCollection->add($oldUnixExtraField); + + $oldExtendedCollection = clone $oldUnixExtraFieldCollection; + $oldExtendedCollection->add($extendedTimestampExtraField); + + $fullCollection = clone $oldExtendedCollection; + $fullCollection->add($ntfsExtra); + + return [ + [ + $ntfsTimeCollection, + $ntfsExtra->getModifyDateTime(), + $ntfsExtra->getAccessDateTime(), + $ntfsExtra->getCreateDateTime(), + ], + [ + $extendedTimestampCollection, + $extendedTimestampExtraField->getModifyDateTime(), + $extendedTimestampExtraField->getAccessDateTime(), + $extendedTimestampExtraField->getCreateDateTime(), + ], + [ + $oldUnixExtraFieldCollection, + $oldUnixExtraField->getModifyDateTime(), + $oldUnixExtraField->getAccessDateTime(), + null, + ], + [ + $oldExtendedCollection, + $extendedTimestampExtraField->getModifyDateTime(), + $extendedTimestampExtraField->getAccessDateTime(), + $extendedTimestampExtraField->getCreateDateTime(), + ], + [ + $fullCollection, + $ntfsExtra->getModifyDateTime(), + $ntfsExtra->getAccessDateTime(), + $ntfsExtra->getCreateDateTime(), + ], + ]; + } + + /** + * @throws ZipException + */ + public function testClone() + { + $newUnixExtra = new NewUnixExtraField(); + $zipData = new ZipFileData(new \SplFileInfo(__FILE__)); + + $zipEntry = new ZipEntry('entry'); + $zipEntry->addExtraField($newUnixExtra); + $zipEntry->setData($zipData); + + $cloneEntry = clone $zipEntry; + + static::assertNotSame($cloneEntry, $zipEntry); + static::assertNotSame($cloneEntry->getCdExtraFields(), $zipEntry->getCdExtraFields()); + static::assertNotSame($cloneEntry->getLocalExtraFields(), $zipEntry->getLocalExtraFields()); + static::assertNotSame($cloneEntry->getCdExtraField(NewUnixExtraField::HEADER_ID), $newUnixExtra); + static::assertNotSame($cloneEntry->getLocalExtraField(NewUnixExtraField::HEADER_ID), $newUnixExtra); + static::assertNotSame($cloneEntry->getData(), $zipData); + } + + public function testExtraCollection() + { + $zipEntry = new ZipEntry('entry'); + $cdCollection = $zipEntry->getCdExtraFields(); + $localCollection = $zipEntry->getLocalExtraFields(); + + static::assertNotSame($cdCollection, $localCollection); + + $anotherCollection = new ExtraFieldsCollection(); + $anotherCollection->add(new JarMarkerExtraField()); + $anotherCollection->add(new AsiExtraField(0100777, 1000, 1000)); + + $zipEntry->setCdExtraFields($anotherCollection); + static::assertSame($anotherCollection, $zipEntry->getCdExtraFields()); + static::assertSame($localCollection, $zipEntry->getLocalExtraFields()); + + $zipEntry->setLocalExtraFields($anotherCollection); + static::assertSame($anotherCollection, $zipEntry->getLocalExtraFields()); + static::assertSame($zipEntry->getCdExtraFields(), $zipEntry->getLocalExtraFields()); + + $newUnixExtraField = new NewUnixExtraField(1, 1000, 1000); + $zipEntry->getCdExtraFields()->add($newUnixExtraField); + + static::assertSame($zipEntry->getCdExtraField(NewUnixExtraField::HEADER_ID), $newUnixExtraField); + static::assertSame($zipEntry->getLocalExtraField(NewUnixExtraField::HEADER_ID), $newUnixExtraField); + } +} diff --git a/tests/ZipFileTest.php b/tests/ZipFileTest.php index b98baaf..9c99f4b 100644 --- a/tests/ZipFileTest.php +++ b/tests/ZipFileTest.php @@ -2389,4 +2389,33 @@ class ZipFileTest extends ZipTestCase } $zipFile->close(); } + + /** + * @throws ZipException + */ + public function testRenameWithRecompressData() + { + $entryName = 'file.txt'; + $newEntryName = 'rename_file.txt'; + $contents = str_repeat('Test' . \PHP_EOL, 1024); + + $zipFile = new ZipFile(); + $zipFile->addFromString($entryName, $contents, ZipCompressionMethod::DEFLATED); + $zipFile->saveAsFile($this->outputFilename); + $zipFile->close(); + + self::assertCorrectZipArchive($this->outputFilename); + + $zipFile->openFile($this->outputFilename); + $zipFile->rename($entryName, $newEntryName); + $zipFile->setCompressionMethodEntry($newEntryName, ZipCompressionMethod::STORED); + $zipFile->saveAsFile($this->outputFilename); + $zipFile->close(); + + self::assertCorrectZipArchive($this->outputFilename); + + $zipFile->openFile($this->outputFilename); + static::assertSame($zipFile->getEntry($newEntryName)->getCompressionMethod(), ZipCompressionMethod::STORED); + $zipFile->close(); + } } diff --git a/tests/ZipPasswordTest.php b/tests/ZipPasswordTest.php index f985f22..3690496 100644 --- a/tests/ZipPasswordTest.php +++ b/tests/ZipPasswordTest.php @@ -57,8 +57,7 @@ class ZipPasswordTest extends ZipFileSetTestCase try { $zipFile[$entryName]; static::fail('Expected Exception has not been raised.'); - } catch (ZipAuthenticationException $ae) { - static::assertContains('Invalid password', $ae->getMessage()); + } catch (ZipException $e) { } }