From 452c5920dd505e0040bae0bdac455dd95b0ea758 Mon Sep 17 00:00:00 2001
From: wapplay-home-linux
+ * An offset to check for.
+ *
+ * The return value will be casted to boolean if non-boolean was returned. + * @since 5.0.0 + */ + public function offsetExists($offset) + { + return isset($this->collection[$offset]); + } + + /** + * Offset to retrieve + * @link http://php.net/manual/en/arrayaccess.offsetget.php + * @param mixed $offset
+ * The offset to retrieve. + *
+ * @return mixed Can return all value types. + * @since 5.0.0 + */ + public function offsetGet($offset) + { + return $this->get($offset); + } + + /** + * Offset to set + * @link http://php.net/manual/en/arrayaccess.offsetset.php + * @param mixed $offset+ * The offset to assign the value to. + *
+ * @param mixed $value+ * The value to set. + *
+ * @return void + * @throws InvalidArgumentException + * @since 5.0.0 + */ + public function offsetSet($offset, $value) + { + if ($value instanceof ExtraField) { + assert($offset == $value::getHeaderId()); + $this->add($value); + } else { + throw new InvalidArgumentException('value is not instanceof ' . ExtraField::class); + } + } + + /** + * Offset to unset + * @link http://php.net/manual/en/arrayaccess.offsetunset.php + * @param mixed $offset+ * The offset to unset. + *
+ * @return void + * @since 5.0.0 + */ + public function offsetUnset($offset) + { + $this->remove($offset); + } + + /** + * Return the current element + * @link http://php.net/manual/en/iterator.current.php + * @return mixed Can return any type. + * @since 5.0.0 + */ + public function current() + { + return current($this->collection); + } + + /** + * Move forward to next element + * @link http://php.net/manual/en/iterator.next.php + * @return void Any returned value is ignored. + * @since 5.0.0 + */ + public function next() + { + next($this->collection); + } + + /** + * Return the key of the current element + * @link http://php.net/manual/en/iterator.key.php + * @return mixed scalar on success, or null on failure. + * @since 5.0.0 + */ + public function key() + { + return key($this->collection); + } + + /** + * Checks if current position is valid + * @link http://php.net/manual/en/iterator.valid.php + * @return boolean The return value will be casted to boolean and then evaluated. + * Returns true on success or false on failure. + * @since 5.0.0 + */ + public function valid() + { + return $this->offsetExists($this->key()); + } + + /** + * Rewind the Iterator to the first element + * @link http://php.net/manual/en/iterator.rewind.php + * @return void Any returned value is ignored. + * @since 5.0.0 + */ + public function rewind() + { + reset($this->collection); + } + + /** + * If clone extra fields. + */ + public function __clone() + { + foreach ($this->collection as $k => $v) { + $this->collection[$k] = clone $v; + } + } +} diff --git a/src/PhpZip/Extra/ExtraFieldsFactory.php b/src/PhpZip/Extra/ExtraFieldsFactory.php new file mode 100644 index 0000000..2e9fd82 --- /dev/null +++ b/src/PhpZip/Extra/ExtraFieldsFactory.php @@ -0,0 +1,100 @@ + $headerId || $headerId > 0xffff) { + throw new ZipException('headerId out of range'); + } + + /** + * @var ExtraField $extraField + */ + if (isset(self::getRegistry()[$headerId])) { + $extraClassName = self::getRegistry()[$headerId]; + $extraField = new $extraClassName; + if ($extraField::getHeaderId() !== $headerId) { + throw new ZipException('Runtime error support headerId ' . $headerId); + } + } else { + $extraField = new DefaultExtraField($headerId); + } + return $extraField; + } + + /** + * Registered extra field classes. + * + * @return array + */ + protected static function getRegistry() + { + if (null === self::$registry) { + self::$registry[WinZipAesEntryExtraField::getHeaderId()] = WinZipAesEntryExtraField::class; + self::$registry[NtfsExtraField::getHeaderId()] = NtfsExtraField::class; + self::$registry[Zip64ExtraField::getHeaderId()] = Zip64ExtraField::class; + } + return self::$registry; + } + + /** + * @return WinZipAesEntryExtraField + */ + public static function createWinZipAesEntryExtra() + { + return new WinZipAesEntryExtraField(); + } + + /** + * @return NtfsExtraField + */ + public static function createNtfsExtra() + { + return new NtfsExtraField(); + } + + /** + * @param ZipEntry $entry + * @return Zip64ExtraField + */ + public static function createZip64Extra(ZipEntry $entry) + { + return new Zip64ExtraField($entry); + } +} diff --git a/src/PhpZip/Extra/Fields/DefaultExtraField.php b/src/PhpZip/Extra/Fields/DefaultExtraField.php new file mode 100644 index 0000000..77af380 --- /dev/null +++ b/src/PhpZip/Extra/Fields/DefaultExtraField.php @@ -0,0 +1,71 @@ + $headerId || $headerId > 0xffff) { + throw new ZipException('headerId out of range'); + } + self::$headerId = $headerId; + } + + /** + * Returns the Header ID (type) of this Extra Field. + * The Header ID is an unsigned short integer (two bytes) + * which must be constant during the life cycle of this object. + * + * @return int + */ + public static function getHeaderId() + { + return self::$headerId & 0xffff; + } + + /** + * Serializes a Data Block. + * @return string + */ + public function serialize() + { + return $this->data; + } + + /** + * Initializes this Extra Field by deserializing a Data Block. + * @param string $data + */ + public function deserialize($data) + { + $this->data = $data; + } +} diff --git a/src/PhpZip/Extra/Fields/NtfsExtraField.php b/src/PhpZip/Extra/Fields/NtfsExtraField.php new file mode 100644 index 0000000..45efb36 --- /dev/null +++ b/src/PhpZip/Extra/Fields/NtfsExtraField.php @@ -0,0 +1,133 @@ +mtime = PackUtil::unpackLongLE(substr($tagData, 0, 8)) / 10000000 - 11644473600; + $this->atime = PackUtil::unpackLongLE(substr($tagData, 8, 8)) / 10000000 - 11644473600; + $this->ctime = PackUtil::unpackLongLE(substr($tagData, 16, 8)) / 10000000 - 11644473600; + } + } + + /** + * Serializes a Data Block. + * @return string + */ + public function serialize() + { + $serialize = ''; + if (null !== $this->mtime && null !== $this->atime && null !== $this->ctime) { + $mtimeLong = ($this->mtime + 11644473600) * 10000000; + $atimeLong = ($this->atime + 11644473600) * 10000000; + $ctimeLong = ($this->ctime + 11644473600) * 10000000; + + $serialize .= pack('Vvv', 0, 1, 8 * 3) + . PackUtil::packLongLE($mtimeLong) + . PackUtil::packLongLE($atimeLong) + . PackUtil::packLongLE($ctimeLong); + } + return $serialize; + } + + /** + * @return int + */ + public function getMtime() + { + return $this->mtime; + } + + /** + * @param int $mtime + */ + public function setMtime($mtime) + { + $this->mtime = (int)$mtime; + } + + /** + * @return int + */ + public function getAtime() + { + return $this->atime; + } + + /** + * @param int $atime + */ + public function setAtime($atime) + { + $this->atime = (int)$atime; + } + + /** + * @return int + */ + public function getCtime() + { + return $this->ctime; + } + + /** + * @param int $ctime + */ + public function setCtime($ctime) + { + $this->ctime = (int)$ctime; + } +} diff --git a/src/PhpZip/Extra/WinZipAesEntryExtraField.php b/src/PhpZip/Extra/Fields/WinZipAesEntryExtraField.php similarity index 65% rename from src/PhpZip/Extra/WinZipAesEntryExtraField.php rename to src/PhpZip/Extra/Fields/WinZipAesEntryExtraField.php index 6f32ce2..83b5e97 100644 --- a/src/PhpZip/Extra/WinZipAesEntryExtraField.php +++ b/src/PhpZip/Extra/Fields/WinZipAesEntryExtraField.php @@ -1,7 +1,10 @@ 0x01, self::KEY_STRENGTH_192BIT => 0x02, self::KEY_STRENGTH_256BIT => 0x03 ]; + protected static $encryptionMethods = [ + self::KEY_STRENGTH_128BIT => ZipFileInterface::ENCRYPTION_METHOD_WINZIP_AES_128, + self::KEY_STRENGTH_192BIT => ZipFileInterface::ENCRYPTION_METHOD_WINZIP_AES_192, + self::KEY_STRENGTH_256BIT => ZipFileInterface::ENCRYPTION_METHOD_WINZIP_AES_256 + ]; + /** * Vendor version. * * @var int */ - private $vendorVersion = self::VV_AE_1; + protected $vendorVersion = self::VV_AE_1; /** * Encryption strength. * * @var int */ - private $encryptionStrength = self::KEY_STRENGTH_256BIT; + protected $encryptionStrength = self::KEY_STRENGTH_256BIT; /** * Zip compression method. * * @var int */ - private $method; + protected $method; /** * Returns the Header ID (type) of this Extra Field. @@ -71,21 +80,6 @@ class WinZipAesEntryExtraField extends ExtraField return 0x9901; } - /** - * Returns the Data Size of this Extra Field. - * The Data Size is an unsigned short integer (two bytes) - * which indicates the length of the Data Block in bytes and does not - * include its own size in this Extra Field. - * This property may be initialized by calling ExtraField::readFrom. - * - * @return int The size of the Data Block in bytes - * or 0 if unknown. - */ - public function getDataSize() - { - return self::DATA_SIZE; - } - /** * Returns the vendor version. * @@ -155,6 +149,32 @@ class WinZipAesEntryExtraField extends ExtraField return $this->method & 0xffff; } + /** + * Internal encryption method. + * + * @return int + */ + public function getEncryptionMethod() + { + return isset(self::$encryptionMethods[$this->getKeyStrength()]) ? + self::$encryptionMethods[$this->getKeyStrength()] : + self::$encryptionMethods[self::KEY_STRENGTH_256BIT]; + } + + /** + * @param int $encryptionMethod + * @return int + * @throws ZipException + */ + public static function getKeyStrangeFromEncryptionMethod($encryptionMethod) + { + $flipKey = array_flip(self::$encryptionMethods); + if (!isset($flipKey[$encryptionMethod])) { + throw new ZipException("Unsupport encryption method " . $encryptionMethod); + } + return $flipKey[$encryptionMethod]; + } + /** * Sets compression method. * @@ -169,37 +189,6 @@ class WinZipAesEntryExtraField extends ExtraField $this->method = $compressionMethod; } - /** - * Initializes this Extra Field by deserializing a Data Block of - * size bytes $size from the resource $handle at the zero based offset $off. - * - * @param resource $handle - * @param int $off Offset bytes - * @param int $size Size - * @throws ZipException - */ - public function readFrom($handle, $off, $size) - { - if (self::DATA_SIZE != $size) - throw new ZipException(); - - fseek($handle, $off, SEEK_SET); - /** - * @var int $vendorVersion - * @var int $vendorId - * @var int $keyStrength - * @var int $method - */ - $unpack = unpack('vvendorVersion/vvendorId/ckeyStrength/vmethod', fread($handle, 7)); - extract($unpack); - $this->setVendorVersion($vendorVersion); - if (self::VENDOR_ID != $vendorId) { - throw new ZipException(); - } - $this->setKeyStrength(self::keyStrength($keyStrength)); // checked - $this->setMethod($method); - } - /** * Set key strength. * @@ -218,19 +207,50 @@ class WinZipAesEntryExtraField extends ExtraField */ public static function encryptionStrength($keyStrength) { - return isset(self::$keyStrengths[$keyStrength]) ? self::$keyStrengths[$keyStrength] : self::$keyStrengths[self::KEY_STRENGTH_128BIT]; + return isset(self::$keyStrengths[$keyStrength]) ? + self::$keyStrengths[$keyStrength] : + self::$keyStrengths[self::KEY_STRENGTH_128BIT]; } /** - * Serializes a Data Block of ExtraField::getDataSize bytes to the - * resource $handle at the zero based offset $off. - * - * @param resource $handle - * @param int $off Offset bytes + * Serializes a Data Block. + * @return string */ - public function writeTo($handle, $off) + public function serialize() { - fseek($handle, $off, SEEK_SET); - fwrite($handle, pack('vvcv', $this->vendorVersion, self::VENDOR_ID, $this->encryptionStrength, $this->method)); + return pack( + 'vvcv', + $this->vendorVersion, + self::VENDOR_ID, + $this->encryptionStrength, + $this->method + ); } -} \ No newline at end of file + + /** + * Initializes this Extra Field by deserializing a Data Block. + * @param string $data + * @throws ZipException + */ + public function deserialize($data) + { + $size = strlen($data); + if (self::DATA_SIZE !== $size) { + throw new ZipException('WinZip AES Extra data invalid size: ' . $size . '. Must be ' . self::DATA_SIZE); + } + + /** + * @var int $vendorVersion + * @var int $vendorId + * @var int $keyStrength + * @var int $method + */ + $unpack = unpack('vvendorVersion/vvendorId/ckeyStrength/vmethod', $data); + $this->setVendorVersion($unpack['vendorVersion']); + if (self::VENDOR_ID !== $unpack['vendorId']) { + throw new ZipException('Vendor id invalid: ' . $unpack['vendorId'] . '. Must be ' . self::VENDOR_ID); + } + $this->setKeyStrength(self::keyStrength($unpack['keyStrength'])); // checked + $this->setMethod($unpack['method']); + } +} diff --git a/src/PhpZip/Extra/Fields/Zip64ExtraField.php b/src/PhpZip/Extra/Fields/Zip64ExtraField.php new file mode 100644 index 0000000..52c3e38 --- /dev/null +++ b/src/PhpZip/Extra/Fields/Zip64ExtraField.php @@ -0,0 +1,118 @@ +setEntry($entry); + } + } + + /** + * @param ZipEntry $entry + */ + public function setEntry(ZipEntry $entry) + { + $this->entry = $entry; + } + + /** + * Returns the Header ID (type) of this Extra Field. + * The Header ID is an unsigned short integer (two bytes) + * which must be constant during the life cycle of this object. + * + * @return int + */ + public static function getHeaderId() + { + return 0x0001; + } + + /** + * Serializes a Data Block. + * @return string + * @throws RuntimeException + */ + public function serialize() + { + if (null === $this->entry) { + throw new RuntimeException("entry is null"); + } + $data = ''; + // Write out Uncompressed Size. + $size = $this->entry->getSize(); + if (0xffffffff <= $size) { + $data .= PackUtil::packLongLE($size); + } + // Write out Compressed Size. + $compressedSize = $this->entry->getCompressedSize(); + if (0xffffffff <= $compressedSize) { + $data .= PackUtil::packLongLE($compressedSize); + } + // Write out Relative Header Offset. + $offset = $this->entry->getOffset(); + if (0xffffffff <= $offset) { + $data .= PackUtil::packLongLE($offset); + } + return $data; + } + + /** + * Initializes this Extra Field by deserializing a Data Block. + * @param string $data + * @throws RuntimeException + */ + public function deserialize($data) + { + if (null === $this->entry) { + throw new RuntimeException("entry is null"); + } + $off = 0; + // Read in Uncompressed Size. + $size = $this->entry->getSize(); + if (0xffffffff <= $size) { + assert(0xffffffff === $size); + $this->entry->setSize(PackUtil::unpackLongLE(substr($data, $off, 8))); + $off += 8; + } + // Read in Compressed Size. + $compressedSize = $this->entry->getCompressedSize(); + if (0xffffffff <= $compressedSize) { + assert(0xffffffff === $compressedSize); + $this->entry->setCompressedSize(PackUtil::unpackLongLE(substr($data, $off, 8))); + $off += 8; + } + // Read in Relative Header Offset. + $offset = $this->entry->getOffset(); + if (0xffffffff <= $offset) { + assert(0xffffffff, $offset); + $this->entry->setOffset(PackUtil::unpackLongLE(substr($data, $off, 8))); + } + } +} diff --git a/src/PhpZip/Extra/NtfsExtraField.php b/src/PhpZip/Extra/NtfsExtraField.php deleted file mode 100644 index da09225..0000000 --- a/src/PhpZip/Extra/NtfsExtraField.php +++ /dev/null @@ -1,176 +0,0 @@ -rawData); - } - - /** - * Initializes this Extra Field by deserializing a Data Block of - * size bytes $size from the resource $handle at the zero based offset $off. - * - * @param resource $handle - * @param int $off Offset bytes - * @param int $size Size - * @throws ZipException If size out of range - */ - public function readFrom($handle, $off, $size) - { - if (0x0000 > $size || $size > 0xffff) { - throw new ZipException('size out of range'); - } - if ($size > 0) { - $off += 4; - fseek($handle, $off, SEEK_SET); - - $unpack = unpack('vtag/vsizeAttr', fread($handle, 4)); - if (24 === $unpack['sizeAttr']) { - $tagData = fread($handle, $unpack['sizeAttr']); - - $this->mtime = PackUtil::unpackLongLE(substr($tagData, 0, 8)) / 10000000 - 11644473600; - $this->atime = PackUtil::unpackLongLE(substr($tagData, 8, 8)) / 10000000 - 11644473600; - $this->ctime = PackUtil::unpackLongLE(substr($tagData, 16, 8)) / 10000000 - 11644473600; - } - $off += $unpack['sizeAttr']; - - if ($size > $off) { - $this->rawData .= fread($handle, $size - $off); - } - } - } - - /** - * Serializes a Data Block of ExtraField::getDataSize bytes to the - * resource $handle at the zero based offset $off. - * - * @param resource $handle - * @param int $off Offset bytes - */ - public function writeTo($handle, $off) - { - if (null !== $this->mtime && null !== $this->atime && null !== $this->ctime) { - fseek($handle, $off, SEEK_SET); - fwrite($handle, pack('Vvv', 0, 1, 8 * 3 + strlen($this->rawData))); - $mtimeLong = ($this->mtime + 11644473600) * 10000000; - fwrite($handle, PackUtil::packLongLE($mtimeLong)); - $atimeLong = ($this->atime + 11644473600) * 10000000; - fwrite($handle, PackUtil::packLongLE($atimeLong)); - $ctimeLong = ($this->ctime + 11644473600) * 10000000; - fwrite($handle, PackUtil::packLongLE($ctimeLong)); - if (!empty($this->rawData)) { - fwrite($handle, $this->rawData); - } - } - } - - /** - * @return int - */ - public function getMtime() - { - return $this->mtime; - } - - /** - * @param int $mtime - */ - public function setMtime($mtime) - { - $this->mtime = (int)$mtime; - } - - /** - * @return int - */ - public function getAtime() - { - return $this->atime; - } - - /** - * @param int $atime - */ - public function setAtime($atime) - { - $this->atime = (int)$atime; - } - - /** - * @return int - */ - public function getCtime() - { - return $this->ctime; - } - - /** - * @param int $ctime - */ - public function setCtime($ctime) - { - $this->ctime = (int)$ctime; - } - -} \ No newline at end of file diff --git a/src/PhpZip/Mapper/OffsetPositionMapper.php b/src/PhpZip/Mapper/OffsetPositionMapper.php index 038cc47..7ea9116 100644 --- a/src/PhpZip/Mapper/OffsetPositionMapper.php +++ b/src/PhpZip/Mapper/OffsetPositionMapper.php @@ -1,4 +1,5 @@ offset = $offset; + $this->offset = (int)$offset; } /** @@ -39,4 +40,4 @@ class OffsetPositionMapper extends PositionMapper { return parent::unmap($position) - $this->offset; } -} \ No newline at end of file +} diff --git a/src/PhpZip/Mapper/PositionMapper.php b/src/PhpZip/Mapper/PositionMapper.php index a7b02a4..e5d67c8 100644 --- a/src/PhpZip/Mapper/PositionMapper.php +++ b/src/PhpZip/Mapper/PositionMapper.php @@ -1,4 +1,5 @@ endOfCentralDirectory = new EndOfCentralDirectory(); - } - - /** - * Reads the central directory from the given seekable byte channel - * and populates the internal tables with ZipEntry instances. - * - * The ZipEntry's will know all data that can be obtained from the - * central directory alone, but not the data that requires the local - * file header or additional data to be read. - * - * @param resource $inputStream - * @throws ZipException - */ - public function mountCentralDirectory($inputStream) - { - $this->modifiedEntries = []; - $this->checkZipFileSignature($inputStream); - $this->endOfCentralDirectory->findCentralDirectory($inputStream); - - $numEntries = $this->endOfCentralDirectory->getCentralDirectoryEntriesSize(); - $entries = []; - for (; $numEntries > 0; $numEntries--) { - $entry = new ZipReadEntry($inputStream); - $entry->setCentralDirectory($this); - // Re-load virtual offset after ZIP64 Extended Information - // Extra Field may have been parsed, map it to the real - // offset and conditionally update the preamble size from it. - $lfhOff = $this->endOfCentralDirectory->getMapper()->map($entry->getOffset()); - if ($lfhOff < $this->endOfCentralDirectory->getPreamble()) { - $this->endOfCentralDirectory->setPreamble($lfhOff); - } - $entries[$entry->getName()] = $entry; - } - - if (0 !== $numEntries % 0x10000) { - throw new ZipException("Expected " . abs($numEntries) . - ($numEntries > 0 ? " more" : " less") . - " entries in the Central Directory!"); - } - $this->entries = $entries; - - if ($this->endOfCentralDirectory->getPreamble() + $this->endOfCentralDirectory->getPostamble() >= fstat($inputStream)['size']) { - assert(0 === $numEntries); - $this->checkZipFileSignature($inputStream); - } - } - - /** - * Check zip file signature - * - * @param resource $inputStream - * @throws ZipException if this not .ZIP file. - */ - private function checkZipFileSignature($inputStream) - { - rewind($inputStream); - // Constraint: A ZIP file must start with a Local File Header - // or a (ZIP64) End Of Central Directory Record if it's empty. - $signatureBytes = fread($inputStream, 4); - if (strlen($signatureBytes) < 4) { - throw new ZipException("Invalid zip file."); - } - $signature = unpack('V', $signatureBytes)[1]; - if ( - ZipEntry::LOCAL_FILE_HEADER_SIG !== $signature - && EndOfCentralDirectory::ZIP64_END_OF_CENTRAL_DIRECTORY_RECORD_SIG !== $signature - && EndOfCentralDirectory::END_OF_CENTRAL_DIRECTORY_RECORD_SIG !== $signature - ) { - throw new ZipException("Expected Local File Header or (ZIP64) End Of Central Directory Record! Signature: " . $signature); - } - } - - /** - * Set compression method for new or rewrites entries. - * @param int $compressionLevel - * @throws InvalidArgumentException - * @see ZipFile::LEVEL_DEFAULT_COMPRESSION - * @see ZipFile::LEVEL_BEST_SPEED - * @see ZipFile::LEVEL_BEST_COMPRESSION - */ - public function setCompressionLevel($compressionLevel = ZipFile::LEVEL_DEFAULT_COMPRESSION) - { - if ($compressionLevel < ZipFile::LEVEL_DEFAULT_COMPRESSION || - $compressionLevel > ZipFile::LEVEL_BEST_COMPRESSION - ) { - throw new InvalidArgumentException('Invalid compression level. Minimum level ' . - ZipFile::LEVEL_DEFAULT_COMPRESSION . '. Maximum level ' . ZipFile::LEVEL_BEST_COMPRESSION); - } - $this->compressionLevel = $compressionLevel; - } - - /** - * @return ZipEntry[] - */ - public function &getEntries() - { - return $this->entries; - } - - /** - * @param string $entryName - * @return ZipEntry - * @throws ZipNotFoundEntry - */ - public function getEntry($entryName) - { - if (!isset($this->entries[$entryName])) { - throw new ZipNotFoundEntry('Zip entry ' . $entryName . ' not found'); - } - return $this->entries[$entryName]; - } - - /** - * @param string $entryName - * @return ZipEntry - * @throws ZipNotFoundEntry - */ - public function getModifiedEntry($entryName){ - if (!isset($this->modifiedEntries[$entryName])) { - throw new ZipNotFoundEntry('Zip modified entry ' . $entryName . ' not found'); - } - return $this->modifiedEntries[$entryName]; - } - - /** - * @return EndOfCentralDirectory - */ - public function getEndOfCentralDirectory() - { - return $this->endOfCentralDirectory; - } - - public function getArchiveComment() - { - return null === $this->endOfCentralDirectory->getComment() ? - '' : - $this->endOfCentralDirectory->getComment(); - } - - /** - * Set entry comment - * @param string $entryName - * @param string|null $comment - * @throws ZipNotFoundEntry - */ - public function setEntryComment($entryName, $comment) - { - if (isset($this->modifiedEntries[$entryName])) { - $this->modifiedEntries[$entryName]->setComment($comment); - } elseif (isset($this->entries[$entryName])) { - $entry = clone $this->entries[$entryName]; - $entry->setComment($comment); - $this->putInModified($entryName, $entry); - } else { - throw new ZipNotFoundEntry("Not found entry " . $entryName); - } - } - - /** - * @param string|null $password - * @param int|null $encryptionMethod - */ - public function setNewPassword($password, $encryptionMethod = null) - { - $this->password = $password; - $this->encryptionMethod = $encryptionMethod; - $this->clearPassword = $password === null; - } - - /** - * @return int|null - */ - public function getZipAlign() - { - return $this->zipAlign; - } - - /** - * @param int|null $zipAlign - */ - public function setZipAlign($zipAlign = null) - { - if (null === $zipAlign) { - $this->zipAlign = null; - return; - } - $this->zipAlign = (int)$zipAlign; - } - - /** - * Put modification or new entries. - * - * @param $entryName - * @param ZipEntry $entry - */ - public function putInModified($entryName, ZipEntry $entry) - { - $this->modifiedEntries[$entryName] = $entry; - } - - /** - * @param string $entryName - * @throws ZipNotFoundEntry - */ - public function deleteEntry($entryName) - { - if (isset($this->entries[$entryName])) { - $this->modifiedEntries[$entryName] = null; - } elseif (isset($this->modifiedEntries[$entryName])) { - unset($this->modifiedEntries[$entryName]); - } else { - throw new ZipNotFoundEntry("Not found entry " . $entryName); - } - } - - /** - * @param string $regexPattern - * @return bool - */ - public function deleteEntriesFromRegex($regexPattern) - { - $count = 0; - foreach ($this->modifiedEntries as $entryName => &$entry) { - if (preg_match($regexPattern, $entryName)) { - unset($entry); - $count++; - } - } - foreach ($this->entries as $entryName => $entry) { - if (preg_match($regexPattern, $entryName)) { - $this->modifiedEntries[$entryName] = null; - $count++; - } - } - return $count > 0; - } - - /** - * @param string $oldName - * @param string $newName - * @throws InvalidArgumentException - * @throws ZipNotFoundEntry - */ - public function rename($oldName, $newName) - { - $oldName = (string)$oldName; - $newName = (string)$newName; - - if (isset($this->entries[$newName]) || isset($this->modifiedEntries[$newName])) { - throw new InvalidArgumentException("New entry name " . $newName . ' is exists.'); - } - - if (isset($this->modifiedEntries[$oldName]) || isset($this->entries[$oldName])) { - $newEntry = clone (isset($this->modifiedEntries[$oldName]) ? - $this->modifiedEntries[$oldName] : - $this->entries[$oldName]); - $newEntry->setName($newName); - - $this->modifiedEntries[$oldName] = null; - $this->modifiedEntries[$newName] = $newEntry; - return; - } - throw new ZipNotFoundEntry("Not found entry " . $oldName); - } - - /** - * Delete all entries. - */ - public function deleteAll() - { - $this->modifiedEntries = []; - foreach ($this->entries as $entry) { - $this->modifiedEntries[$entry->getName()] = null; - } - } - - /** - * @param resource $outputStream - */ - public function writeArchive($outputStream) - { - /** - * @var ZipEntry[] $memoryEntriesResult - */ - $memoryEntriesResult = []; - foreach ($this->entries as $entryName => $entry) { - if (isset($this->modifiedEntries[$entryName])) continue; - - if ( - (null !== $this->password || $this->clearPassword) && - $entry->isEncrypted() && - $entry->getPassword() !== null && - ( - $entry->getPassword() !== $this->password || - $entry->getEncryptionMethod() !== $this->encryptionMethod - ) - ) { - $prototypeEntry = new ZipNewStringEntry($entry->getEntryContent()); - $prototypeEntry->setName($entry->getName()); - $prototypeEntry->setMethod($entry->getMethod()); - $prototypeEntry->setTime($entry->getTime()); - $prototypeEntry->setExternalAttributes($entry->getExternalAttributes()); - $prototypeEntry->setExtra($entry->getExtra()); - $prototypeEntry->setPassword($this->password, $this->encryptionMethod); - if ($this->clearPassword) { - $prototypeEntry->clearEncryption(); - } - } else { - $prototypeEntry = clone $entry; - } - $memoryEntriesResult[$entryName] = $prototypeEntry; - } - - foreach ($this->modifiedEntries as $entryName => $outputEntry) { - if (null === $outputEntry) { // remove marked entry - unset($memoryEntriesResult[$entryName]); - } else { - if (null !== $this->password) { - $outputEntry->setPassword($this->password, $this->encryptionMethod); - } - $memoryEntriesResult[$entryName] = $outputEntry; - } - } - - foreach ($memoryEntriesResult as $key => $outputEntry) { - $outputEntry->setCentralDirectory($this); - $outputEntry->writeEntry($outputStream); - } - $centralDirectoryOffset = ftell($outputStream); - foreach ($memoryEntriesResult as $key => $outputEntry) { - if (!$this->writeCentralFileHeader($outputStream, $outputEntry)) { - unset($memoryEntriesResult[$key]); - } - } - $centralDirectoryEntries = sizeof($memoryEntriesResult); - $this->getEndOfCentralDirectory()->writeEndOfCentralDirectory( - $outputStream, - $centralDirectoryEntries, - $centralDirectoryOffset - ); - } - - /** - * Writes a Central File Header record. - * - * @param resource $outputStream - * @param ZipEntry $entry - * @return bool false if and only if the record has been skipped, - * i.e. not written for some other reason than an I/O error. - */ - private function writeCentralFileHeader($outputStream, ZipEntry $entry) - { - $compressedSize = $entry->getCompressedSize(); - $size = $entry->getSize(); - // This test MUST NOT include the CRC-32 because VV_AE_2 sets it to - // UNKNOWN! - if (ZipEntry::UNKNOWN === ($compressedSize | $size)) { - return false; - } - $extra = $entry->getExtra(); - $extraSize = strlen($extra); - - $commentLength = strlen($entry->getComment()); - fwrite( - $outputStream, - pack( - 'VvvvvVVVVvvvvvVV', - // central file header signature 4 bytes (0x02014b50) - self::CENTRAL_FILE_HEADER_SIG, - // version made by 2 bytes - ($entry->getPlatform() << 8) | 63, - // version needed to extract 2 bytes - $entry->getVersionNeededToExtract(), - // general purpose bit flag 2 bytes - $entry->getGeneralPurposeBitFlags(), - // compression method 2 bytes - $entry->getMethod(), - // last mod file datetime 4 bytes - $entry->getDosTime(), - // crc-32 4 bytes - $entry->getCrc(), - // compressed size 4 bytes - $entry->getCompressedSize(), - // uncompressed size 4 bytes - $entry->getSize(), - // file name length 2 bytes - strlen($entry->getName()), - // extra field length 2 bytes - $extraSize, - // file comment length 2 bytes - $commentLength, - // disk number start 2 bytes - 0, - // internal file attributes 2 bytes - 0, - // external file attributes 4 bytes - $entry->getExternalAttributes(), - // relative offset of local header 4 bytes - $entry->getOffset() - ) - ); - // file name (variable size) - fwrite($outputStream, $entry->getName()); - if (0 < $extraSize) { - // extra field (variable size) - fwrite($outputStream, $extra); - } - if (0 < $commentLength) { - // file comment (variable size) - fwrite($outputStream, $entry->getComment()); - } - return true; - } - - public function release() - { - unset($this->entries); - unset($this->modifiedEntries); - } - - function __destruct() - { - $this->release(); - } - -} \ No newline at end of file diff --git a/src/PhpZip/Model/EndOfCentralDirectory.php b/src/PhpZip/Model/EndOfCentralDirectory.php index 1730c0c..8016e5f 100644 --- a/src/PhpZip/Model/EndOfCentralDirectory.php +++ b/src/PhpZip/Model/EndOfCentralDirectory.php @@ -1,11 +1,6 @@ mapper = new PositionMapper(); - } - - /** - * Positions the file pointer at the first Central File Header. - * Performs some means to check that this is really a ZIP file. - * - * @param resource $inputStream - * @throws ZipException If the file is not compatible to the ZIP File - * Format Specification. - */ - public function findCentralDirectory($inputStream) - { - // Search for End of central directory record. - $stats = fstat($inputStream); - $size = $stats['size']; - $max = $size - self::END_OF_CENTRAL_DIRECTORY_RECORD_MIN_LEN; - $min = $max >= 0xffff ? $max - 0xffff : 0; - for ($endOfCentralDirRecordPos = $max; $endOfCentralDirRecordPos >= $min; $endOfCentralDirRecordPos--) { - fseek($inputStream, $endOfCentralDirRecordPos, SEEK_SET); - // end of central dir signature 4 bytes (0x06054b50) - if (self::END_OF_CENTRAL_DIRECTORY_RECORD_SIG !== unpack('V', fread($inputStream, 4))[1]) - continue; - - // number of this disk - 2 bytes - // number of the disk with the start of the - // central directory - 2 bytes - // total number of entries in the central - // directory on this disk - 2 bytes - // total number of entries in the central - // directory - 2 bytes - // size of the central directory - 4 bytes - // offset of start of central directory with - // respect to the starting disk number - 4 bytes - // ZIP file comment length - 2 bytes - $data = unpack( - 'vdiskNo/vcdDiskNo/vcdEntriesDisk/vcdEntries/VcdSize/VcdPos/vcommentLength', - fread($inputStream, 18) - ); - - if (0 !== $data['diskNo'] || 0 !== $data['cdDiskNo'] || $data['cdEntriesDisk'] !== $data['cdEntries']) { - throw new ZipException( - "ZIP file spanning/splitting is not supported!" - ); - } - // .ZIP file comment (variable size) - if (0 < $data['commentLength']) { - $this->comment = fread($inputStream, $data['commentLength']); - } - $this->preamble = $endOfCentralDirRecordPos; - $this->postamble = $size - ftell($inputStream); - - // Check for ZIP64 End Of Central Directory Locator. - $endOfCentralDirLocatorPos = $endOfCentralDirRecordPos - self::ZIP64_END_OF_CENTRAL_DIRECTORY_LOCATOR_LEN; - - fseek($inputStream, $endOfCentralDirLocatorPos, SEEK_SET); - // zip64 end of central dir locator - // signature 4 bytes (0x07064b50) - if ( - 0 > $endOfCentralDirLocatorPos || - ftell($inputStream) === $size || - self::ZIP64_END_OF_CENTRAL_DIRECTORY_LOCATOR_SIG !== unpack('V', fread($inputStream, 4))[1] - ) { - // Seek and check first CFH, probably requiring an offset mapper. - $offset = $endOfCentralDirRecordPos - $data['cdSize']; - fseek($inputStream, $offset, SEEK_SET); - $offset -= $data['cdPos']; - if (0 !== $offset) { - $this->mapper = new OffsetPositionMapper($offset); - } - $this->centralDirectoryEntriesSize = $data['cdEntries']; - return; - } - - // number of the disk with the - // start of the zip64 end of - // central directory 4 bytes - $zip64EndOfCentralDirectoryRecordDisk = unpack('V', fread($inputStream, 4))[1]; - // relative offset of the zip64 - // end of central directory record 8 bytes - $zip64EndOfCentralDirectoryRecordPos = PackUtil::unpackLongLE(fread($inputStream, 8)); - // total number of disks 4 bytes - $totalDisks = unpack('V', fread($inputStream, 4))[1]; - if (0 !== $zip64EndOfCentralDirectoryRecordDisk || 1 !== $totalDisks) { - throw new ZipException("ZIP file spanning/splitting is not supported!"); - } - fseek($inputStream, $zip64EndOfCentralDirectoryRecordPos, SEEK_SET); - // zip64 end of central dir - // signature 4 bytes (0x06064b50) - $zip64EndOfCentralDirSig = unpack('V', fread($inputStream, 4))[1]; - if (self::ZIP64_END_OF_CENTRAL_DIRECTORY_RECORD_SIG !== $zip64EndOfCentralDirSig) { - throw new ZipException("Expected ZIP64 End Of Central Directory Record!"); - } - // size of zip64 end of central - // directory record 8 bytes - // version made by 2 bytes - // version needed to extract 2 bytes - fseek($inputStream, 12, SEEK_CUR); - // number of this disk 4 bytes - $diskNo = unpack('V', fread($inputStream, 4))[1]; - // number of the disk with the - // start of the central directory 4 bytes - $cdDiskNo = unpack('V', fread($inputStream, 4))[1]; - // total number of entries in the - // central directory on this disk 8 bytes - $cdEntriesDisk = PackUtil::unpackLongLE(fread($inputStream, 8)); - // total number of entries in the - // central directory 8 bytes - $cdEntries = PackUtil::unpackLongLE(fread($inputStream, 8)); - if (0 !== $diskNo || 0 !== $cdDiskNo || $cdEntriesDisk !== $cdEntries) { - throw new ZipException("ZIP file spanning/splitting is not supported!"); - } - if ($cdEntries < 0 || 0x7fffffff < $cdEntries) { - throw new ZipException("Total Number Of Entries In The Central Directory out of range!"); - } - // size of the central directory 8 bytes - fseek($inputStream, 8, SEEK_CUR); - // offset of start of central - // directory with respect to - // the starting disk number 8 bytes - $cdPos = PackUtil::unpackLongLE(fread($inputStream, 8)); - // zip64 extensible data sector (variable size) - fseek($inputStream, $cdPos, SEEK_SET); - $this->preamble = $zip64EndOfCentralDirectoryRecordPos; - $this->centralDirectoryEntriesSize = $cdEntries; - $this->zip64 = true; - return; - } - // Start recovering file entries from min. - $this->preamble = $min; - $this->postamble = $size - $min; - $this->centralDirectoryEntriesSize = 0; + $this->entryCount = $entryCount; + $this->comment = $comment; + $this->zip64 = $zip64; } /** @@ -256,9 +105,9 @@ class EndOfCentralDirectory /** * @return int */ - public function getCentralDirectoryEntriesSize() + public function getEntryCount() { - return $this->centralDirectoryEntriesSize; + return $this->entryCount; } /** @@ -268,152 +117,4 @@ class EndOfCentralDirectory { return $this->zip64; } - - /** - * @return int - */ - public function getPreamble() - { - return $this->preamble; - } - - /** - * @return int - */ - public function getPostamble() - { - return $this->postamble; - } - - /** - * @return PositionMapper - */ - public function getMapper() - { - return $this->mapper; - } - - /** - * @param int $preamble - */ - public function setPreamble($preamble) - { - $this->preamble = $preamble; - } - - /** - * Set archive comment - * @param string|null $comment - * @throws InvalidArgumentException - */ - public function setComment($comment = null) - { - if (null !== $comment && strlen($comment) !== 0) { - $comment = (string)$comment; - $length = strlen($comment); - if (0x0000 > $length || $length > 0xffff) { - throw new InvalidArgumentException('Length comment out of range'); - } - } - $this->modified = $comment !== $this->comment; - $this->newComment = $comment; - } - - /** - * Write end of central directory. - * - * @param resource $outputStream Output stream - * @param int $centralDirectoryEntries Size entries - * @param int $centralDirectoryOffset Offset central directory - */ - public function writeEndOfCentralDirectory($outputStream, $centralDirectoryEntries, $centralDirectoryOffset) - { - $position = ftell($outputStream); - $centralDirectorySize = $position - $centralDirectoryOffset; - $centralDirectoryEntriesZip64 = $centralDirectoryEntries > 0xffff; - $centralDirectorySizeZip64 = $centralDirectorySize > 0xffffffff; - $centralDirectoryOffsetZip64 = $centralDirectoryOffset > 0xffffffff; - $centralDirectoryEntries16 = $centralDirectoryEntriesZip64 ? 0xffff : (int)$centralDirectoryEntries; - $centralDirectorySize32 = $centralDirectorySizeZip64 ? 0xffffffff : $centralDirectorySize; - $centralDirectoryOffset32 = $centralDirectoryOffsetZip64 ? 0xffffffff : $centralDirectoryOffset; - $zip64 // ZIP64 extensions? - = $centralDirectoryEntriesZip64 - || $centralDirectorySizeZip64 - || $centralDirectoryOffsetZip64; - if ($zip64) { - // relative offset of the zip64 end of central directory record - $zip64EndOfCentralDirectoryOffset = $position; - // zip64 end of central dir - // signature 4 bytes (0x06064b50) - fwrite($outputStream, pack('V', self::ZIP64_END_OF_CENTRAL_DIRECTORY_RECORD_SIG)); - // size of zip64 end of central - // directory record 8 bytes - fwrite($outputStream, PackUtil::packLongLE(self::ZIP64_END_OF_CENTRAL_DIRECTORY_RECORD_MIN_LEN - 12)); - // version made by 2 bytes - // version needed to extract 2 bytes - // due to potential use of BZIP2 compression - // number of this disk 4 bytes - // number of the disk with the - // start of the central directory 4 bytes - fwrite($outputStream, pack('vvVV', 63, 46, 0, 0)); - // total number of entries in the - // central directory on this disk 8 bytes - fwrite($outputStream, PackUtil::packLongLE($centralDirectoryEntries)); - // total number of entries in the - // central directory 8 bytes - fwrite($outputStream, PackUtil::packLongLE($centralDirectoryEntries)); - // size of the central directory 8 bytes - fwrite($outputStream, PackUtil::packLongLE($centralDirectorySize)); - // offset of start of central - // directory with respect to - // the starting disk number 8 bytes - fwrite($outputStream, PackUtil::packLongLE($centralDirectoryOffset)); - // zip64 extensible data sector (variable size) - // - // zip64 end of central dir locator - // signature 4 bytes (0x07064b50) - // number of the disk with the - // start of the zip64 end of - // central directory 4 bytes - fwrite($outputStream, pack('VV', self::ZIP64_END_OF_CENTRAL_DIRECTORY_LOCATOR_SIG, 0)); - // relative offset of the zip64 - // end of central directory record 8 bytes - fwrite($outputStream, PackUtil::packLongLE($zip64EndOfCentralDirectoryOffset)); - // total number of disks 4 bytes - fwrite($outputStream, pack('V', 1)); - } - $comment = $this->modified ? $this->newComment : $this->comment; - $commentLength = strlen($comment); - fwrite( - $outputStream, - pack('VvvvvVVv', - // end of central dir signature 4 bytes (0x06054b50) - self::END_OF_CENTRAL_DIRECTORY_RECORD_SIG, - // number of this disk 2 bytes - 0, - // number of the disk with the - // start of the central directory 2 bytes - 0, - // total number of entries in the - // central directory on this disk 2 bytes - $centralDirectoryEntries16, - // total number of entries in - // the central directory 2 bytes - $centralDirectoryEntries16, - // size of the central directory 4 bytes - $centralDirectorySize32, - // offset of start of central - // directory with respect to - // the starting disk number 4 bytes - $centralDirectoryOffset32, - // .ZIP file comment length 2 bytes - $commentLength - ) - ); - if ($commentLength > 0) { - // .ZIP file comment (variable size) - fwrite($outputStream, $comment); - } - } - -} \ No newline at end of file +} diff --git a/src/PhpZip/Model/Entry/OutputOffsetEntry.php b/src/PhpZip/Model/Entry/OutputOffsetEntry.php new file mode 100644 index 0000000..94bd15b --- /dev/null +++ b/src/PhpZip/Model/Entry/OutputOffsetEntry.php @@ -0,0 +1,49 @@ +offset = $pos; + $this->entry = $entry; + } + + /** + * @return int + */ + public function getOffset() + { + return $this->offset; + } + + /** + * @return ZipEntry + */ + public function getEntry() + { + return $this->entry; + } +} diff --git a/src/PhpZip/Model/Entry/ZipAbstractEntry.php b/src/PhpZip/Model/Entry/ZipAbstractEntry.php index 17f8d24..bffe7bb 100644 --- a/src/PhpZip/Model/Entry/ZipAbstractEntry.php +++ b/src/PhpZip/Model/Entry/ZipAbstractEntry.php @@ -4,15 +4,14 @@ namespace PhpZip\Model\Entry; use PhpZip\Exception\InvalidArgumentException; use PhpZip\Exception\ZipException; -use PhpZip\Extra\DefaultExtraField; -use PhpZip\Extra\ExtraField; -use PhpZip\Extra\ExtraFields; -use PhpZip\Extra\WinZipAesEntryExtraField; -use PhpZip\Model\CentralDirectory; +use PhpZip\Extra\ExtraFieldsCollection; +use PhpZip\Extra\ExtraFieldsFactory; +use PhpZip\Extra\Fields\WinZipAesEntryExtraField; +use PhpZip\Extra\Fields\Zip64ExtraField; use PhpZip\Model\ZipEntry; use PhpZip\Util\DateTimeConverter; -use PhpZip\Util\PackUtil; -use PhpZip\ZipFile; +use PhpZip\Util\StringUtil; +use PhpZip\ZipFileInterface; /** * Abstract ZIP entry. @@ -23,16 +22,10 @@ use PhpZip\ZipFile; */ abstract class ZipAbstractEntry implements ZipEntry { - /** - * @var CentralDirectory - */ - private $centralDirectory; - /** * @var int Bit flags for init state. */ private $init; - /** * @var string Entry name (filename in archive) */ @@ -45,14 +38,14 @@ abstract class ZipAbstractEntry implements ZipEntry * @var int */ private $versionNeededToExtract = 20; - /** - * @var int - */ - private $general; /** * @var int Compression method */ private $method; + /** + * @var int + */ + private $general; /** * @var int Dos time */ @@ -78,13 +71,13 @@ abstract class ZipAbstractEntry implements ZipEntry */ private $offset = self::UNKNOWN; /** - * The map of Extra Fields. - * Maps from Header ID [Integer] to Extra Field [ExtraField]. + * Collections of Extra Fields. + * Keys from Header ID [int] and value Extra Field [ExtraField]. * Should be null or may be empty if no Extra Fields are used. * - * @var ExtraFields + * @var ExtraFieldsCollection */ - private $fields; + private $extraFieldsCollection; /** * @var string Comment field. */ @@ -95,55 +88,48 @@ abstract class ZipAbstractEntry implements ZipEntry private $password; /** * Encryption method. - * @see ZipFile::ENCRYPTION_METHOD_TRADITIONAL - * @see ZipFile::ENCRYPTION_METHOD_WINZIP_AES + * @see ZipFileInterface::ENCRYPTION_METHOD_TRADITIONAL + * @see ZipFileInterface::ENCRYPTION_METHOD_WINZIP_AES_128 + * @see ZipFileInterface::ENCRYPTION_METHOD_WINZIP_AES_192 + * @see ZipFileInterface::ENCRYPTION_METHOD_WINZIP_AES_256 * @var int */ - private $encryptionMethod = ZipFile::ENCRYPTION_METHOD_TRADITIONAL; - + private $encryptionMethod = ZipFileInterface::ENCRYPTION_METHOD_TRADITIONAL; /** * @var int */ - private $compressionLevel = ZipFile::LEVEL_DEFAULT_COMPRESSION; + private $compressionLevel = ZipFileInterface::LEVEL_DEFAULT_COMPRESSION; /** - * @param int $mask - * @return bool + * ZipAbstractEntry constructor. */ - private function isInit($mask) + public function __construct() { - return 0 !== ($this->init & $mask); + $this->extraFieldsCollection = new ExtraFieldsCollection(); } /** - * @param int $mask - * @param bool $init + * @param ZipEntry $entry */ - private function setInit($mask, $init) + public function setEntry(ZipEntry $entry) { - if ($init) { - $this->init |= $mask; - } else { - $this->init &= ~$mask; - } - } - - /** - * @return CentralDirectory - */ - public function getCentralDirectory() - { - return $this->centralDirectory; - } - - /** - * @param CentralDirectory $centralDirectory - * @return ZipEntry - */ - public function setCentralDirectory(CentralDirectory $centralDirectory) - { - $this->centralDirectory = $centralDirectory; - return $this; + $this->setName($entry->getName()); + $this->setPlatform($entry->getPlatform()); + $this->setVersionNeededToExtract($entry->getVersionNeededToExtract()); + $this->setMethod($entry->getMethod()); + $this->setGeneralPurposeBitFlags($entry->getGeneralPurposeBitFlags()); + $this->setDosTime($entry->getDosTime()); + $this->setCrc($entry->getCrc()); + $this->setCompressedSize($entry->getCompressedSize()); + $this->setSize($entry->getSize()); + $this->setExternalAttributes($entry->getExternalAttributes()); + $this->setOffset($entry->getOffset()); + $this->setExtra($entry->getExtra()); + $this->setComment($entry->getComment()); + $this->setPassword($entry->getPassword()); + $this->setEncryptionMethod($entry->getEncryptionMethod()); + $this->setCompressionLevel($entry->getCompressionLevel()); + $this->setEncrypted($entry->isEncrypted()); } /** @@ -174,6 +160,23 @@ abstract class ZipAbstractEntry implements ZipEntry return $this; } + /** + * Sets the indexed General Purpose Bit Flag. + * + * @param int $mask + * @param bool $bit + * @return ZipEntry + */ + public function setGeneralPurposeBitFlag($mask, $bit) + { + if ($bit) { + $this->general |= $mask; + } else { + $this->general &= ~$mask; + } + return $this; + } + /** * @return int Get platform */ @@ -204,6 +207,28 @@ abstract class ZipAbstractEntry implements ZipEntry return $this; } + /** + * @param int $mask + * @return bool + */ + protected function isInit($mask) + { + return 0 !== ($this->init & $mask); + } + + /** + * @param int $mask + * @param bool $init + */ + protected function setInit($mask, $init) + { + if ($init) { + $this->init |= $mask; + } else { + $this->init &= ~$mask; + } + } + /** * Version needed to extract. * @@ -321,21 +346,9 @@ abstract class ZipAbstractEntry implements ZipEntry return $this; } - /** - * Returns true if and only if this ZIP entry represents a directory entry - * (i.e. end with '/'). - * - * @return bool - */ - public function isDirectory() - { - return $this->name[strlen($this->name) - 1] === '/'; - } - /** * Returns the General Purpose Bit Flags. - * - * @return bool + * @return int */ public function getGeneralPurposeBitFlags() { @@ -355,33 +368,19 @@ abstract class ZipAbstractEntry implements ZipEntry throw new ZipException('general out of range'); } $this->general = $general; - return $this; - } - - /** - * Returns the indexed General Purpose Bit Flag. - * - * @param int $mask - * @return bool - */ - public function getGeneralPurposeBitFlag($mask) - { - return 0 !== ($this->general & $mask); - } - - /** - * Sets the indexed General Purpose Bit Flag. - * - * @param int $mask - * @param bool $bit - * @return ZipEntry - */ - public function setGeneralPurposeBitFlag($mask, $bit) - { - if ($bit) - $this->general |= $mask; - else - $this->general &= ~$mask; + if ($this->method === ZipFileInterface::METHOD_DEFLATED) { + $bit1 = $this->getGeneralPurposeBitFlag(self::GPBF_COMPRESSION_FLAG1); + $bit2 = $this->getGeneralPurposeBitFlag(self::GPBF_COMPRESSION_FLAG2); + if ($bit1 && !$bit2) { + $this->compressionLevel = ZipFileInterface::LEVEL_BEST_COMPRESSION; + } elseif (!$bit1 && $bit2) { + $this->compressionLevel = ZipFileInterface::LEVEL_FAST; + } elseif ($bit1 && $bit2) { + $this->compressionLevel = ZipFileInterface::LEVEL_SUPER_FAST; + } else { + $this->compressionLevel = ZipFileInterface::LEVEL_DEFAULT_COMPRESSION; + } + } return $this; } @@ -395,6 +394,17 @@ abstract class ZipAbstractEntry implements ZipEntry return $this->getGeneralPurposeBitFlag(self::GPBF_ENCRYPTED); } + /** + * Returns the indexed General Purpose Bit Flag. + * + * @param int $mask + * @return bool + */ + public function getGeneralPurposeBitFlag($mask) + { + return 0 !== ($this->general & $mask); + } + /** * Sets the encryption property to false and removes any other * encryption artifacts. @@ -404,17 +414,16 @@ abstract class ZipAbstractEntry implements ZipEntry public function clearEncryption() { $this->setEncrypted(false); - if (null !== $this->fields) { - $field = $this->fields->get(WinZipAesEntryExtraField::getHeaderId()); - if (null !== $field) { - /** - * @var WinZipAesEntryExtraField $field - */ - $this->removeExtraField(WinZipAesEntryExtraField::getHeaderId()); - } + $headerId = WinZipAesEntryExtraField::getHeaderId(); + if (isset($this->extraFieldsCollection[$headerId])) { + /** + * @var WinZipAesEntryExtraField $field + */ + $field = $this->extraFieldsCollection[$headerId]; if (self::METHOD_WINZIP_AES === $this->getMethod()) { $this->setMethod(null === $field ? self::UNKNOWN : $field->getMethod()); } + unset($this->extraFieldsCollection[$headerId]); } $this->password = null; return $this; @@ -428,6 +437,7 @@ abstract class ZipAbstractEntry implements ZipEntry */ public function setEncrypted($encrypted) { + $encrypted = (bool)$encrypted; $this->setGeneralPurposeBitFlag(self::GPBF_ENCRYPTED, $encrypted); return $this; } @@ -451,6 +461,10 @@ abstract class ZipAbstractEntry implements ZipEntry */ public function setMethod($method) { + if (self::UNKNOWN === $method) { + $this->method = $method; + return $this; + } if (0x0000 > $method || $method > 0xffff) { throw new ZipException('method out of range'); } @@ -458,21 +472,15 @@ abstract class ZipAbstractEntry implements ZipEntry case self::METHOD_WINZIP_AES: $this->method = $method; $this->setInit(self::BIT_METHOD, true); - $this->setEncryptionMethod(ZipFile::ENCRYPTION_METHOD_WINZIP_AES); break; - case ZipFile::METHOD_STORED: - case ZipFile::METHOD_DEFLATED: - case ZipFile::METHOD_BZIP2: + case ZipFileInterface::METHOD_STORED: + case ZipFileInterface::METHOD_DEFLATED: + case ZipFileInterface::METHOD_BZIP2: $this->method = $method; $this->setInit(self::BIT_METHOD, true); break; - case self::UNKNOWN: - $this->method = ZipFile::METHOD_STORED; - $this->setInit(self::BIT_METHOD, false); - break; - default: throw new ZipException($this->name . " (unsupported compression method $method)"); } @@ -492,24 +500,6 @@ abstract class ZipAbstractEntry implements ZipEntry return DateTimeConverter::toUnixTimestamp($this->getDosTime()); } - /** - * Set time from unix timestamp. - * - * @param int $unixTimestamp - * @return ZipEntry - */ - public function setTime($unixTimestamp) - { - $known = self::UNKNOWN != $unixTimestamp; - if ($known) { - $this->dosTime = DateTimeConverter::toDosTime($unixTimestamp); - } else { - $this->dosTime = 0; - } - $this->setInit(self::BIT_DATE_TIME, $known); - return $this; - } - /** * Get Dos Time * @@ -535,6 +525,24 @@ abstract class ZipAbstractEntry implements ZipEntry $this->setInit(self::BIT_DATE_TIME, true); } + /** + * Set time from unix timestamp. + * + * @param int $unixTimestamp + * @return ZipEntry + */ + public function setTime($unixTimestamp) + { + $known = self::UNKNOWN != $unixTimestamp; + if ($known) { + $this->dosTime = DateTimeConverter::toDosTime($unixTimestamp); + } else { + $this->dosTime = 0; + } + $this->setInit(self::BIT_DATE_TIME, $known); + return $this; + } + /** * Returns the external file attributes. * @@ -572,123 +580,43 @@ abstract class ZipAbstractEntry implements ZipEntry } /** - * Return extra field from header id. + * Returns true if and only if this ZIP entry represents a directory entry + * (i.e. end with '/'). * - * @param int $headerId - * @return ExtraField|null - */ - public function getExtraField($headerId) - { - return $this->fields === null ? null : $this->fields->get($headerId); - } - - /** - * Add extra field. - * - * @param ExtraField $field - * @return ExtraField - * @throws ZipException - */ - public function addExtraField($field) - { - if (null === $field) { - throw new ZipException("extra field null"); - } - if (null === $this->fields) { - $this->fields = new ExtraFields(); - } - return $this->fields->add($field); - } - - /** - * Return exists extra field from header id. - * - * @param int $headerId * @return bool */ - public function hasExtraField($headerId) + public function isDirectory() { - return $this->fields === null ? false : $this->fields->has($headerId); + return StringUtil::endsWith($this->name, '/'); } /** - * Remove extra field from header id. - * - * @param int $headerId - * @return ExtraField|null + * @return ExtraFieldsCollection */ - public function removeExtraField($headerId) + public function &getExtraFieldsCollection() { - return null !== $this->fields ? $this->fields->remove($headerId) : null; + return $this->extraFieldsCollection; } /** * Returns a protective copy of the serialized Extra Fields. - * - * @return string A new byte array holding the serialized Extra Fields. - * null is never returned. - */ - public function getExtra() - { - return $this->getExtraFields(false); - } - - /** - * @param bool $zip64 * @return string * @throws ZipException */ - private function getExtraFields($zip64) + public function getExtra() { - if ($zip64) { - $field = $this->composeZip64ExtraField(); - if (null !== $field) { - if (null === $this->fields) { - $this->fields = new ExtraFields(); - } - $this->fields->add($field); - } - } else { - assert(null === $this->fields || null === $this->fields->get(ExtraField::ZIP64_HEADER_ID)); + $extraData = ''; + foreach ($this->getExtraFieldsCollection() as $extraField) { + $data = $extraField->serialize(); + $extraData .= pack('vv', $extraField::getHeaderId(), strlen($data)); + $extraData .= $data; } - return null === $this->fields ? null : $this->fields->getExtra(); - } - /** - * Composes a ZIP64 Extended Information Extra Field from the properties - * of this entry. - * If no ZIP64 Extended Information Extra Field is required it is removed - * from the collection of Extra Fields. - * - * @return ExtraField|null - */ - private function composeZip64ExtraField() - { - $handle = fopen('php://memory', 'r+b'); - // Write out Uncompressed Size. - $size = $this->getSize(); - if (0xffffffff <= $size) { - fwrite($handle, PackUtil::packLongLE($size)); + $size = strlen($extraData); + if (0x0000 > $size || $size > 0xffff) { + throw new ZipException('Size extra out of range: ' . $size . '. Extra data: ' . $extraData); } - // Write out Compressed Size. - $compressedSize = $this->getCompressedSize(); - if (0xffffffff <= $compressedSize) { - fwrite($handle, PackUtil::packLongLE($compressedSize)); - } - // Write out Relative Header Offset. - $offset = $this->getOffset(); - if (0xffffffff <= $offset) { - fwrite($handle, PackUtil::packLongLE($offset)); - } - // Create ZIP64 Extended Information Extra Field from serialized data. - $field = null; - if (ftell($handle) > 0) { - $field = new DefaultExtraField(ExtraField::ZIP64_HEADER_ID); - $field->readFrom($handle, 0, ftell($handle)); - } else { - $field = null; - } - return $field; + return $extraData; } /** @@ -701,99 +629,31 @@ abstract class ZipAbstractEntry implements ZipEntry * * @param string $data The byte array holding the serialized Extra Fields. * @throws ZipException if the serialized Extra Fields exceed 64 KB - * @return ZipEntry - * or do not conform to the ZIP File Format Specification */ public function setExtra($data) { + $this->extraFieldsCollection = new ExtraFieldsCollection(); if (null !== $data) { - $length = strlen($data); - if (0x0000 > $length || $length > 0xffff) { - throw new ZipException("Extra Fields too large"); + $extraLength = strlen($data); + if (0x0000 > $extraLength || $extraLength > 0xffff) { + throw new ZipException("Extra Fields too large: " . $extraLength); } - } - if (null === $data || strlen($data) <= 0) { - $this->fields = null; - } else { - $this->setExtraFields($data, false); - } - return $this; - } - - /** - * @param string $data - * @param bool $zip64 - */ - private function setExtraFields($data, $zip64) - { - if (null === $this->fields) { - $this->fields = new ExtraFields(); - } - $handle = fopen('php://memory', 'r+b'); - fwrite($handle, $data); - rewind($handle); - - $this->fields->readFrom($handle, 0, strlen($data)); - $result = false; - if ($zip64) { - $result = $this->parseZip64ExtraField(); - } - if ($result) { - $this->fields->remove(ExtraField::ZIP64_HEADER_ID); - if ($this->fields->size() <= 0) { - if (0 !== $this->fields->size()) { - $this->fields = null; + $pos = 0; + $endPos = $extraLength; + while ($pos < $endPos) { + $unpack = unpack('vheaderId/vdataSize', substr($data, $pos, 4)); + $pos += 4; + $headerId = (int)$unpack['headerId']; + $dataSize = (int)$unpack['dataSize']; + $extraField = ExtraFieldsFactory::create($headerId); + if ($extraField instanceof Zip64ExtraField) { + $extraField->setEntry($this); } + $extraField->deserialize(substr($data, $pos, $dataSize)); + $pos += $dataSize; + $this->extraFieldsCollection[$headerId] = $extraField; } } - fclose($handle); - } - - /** - * Parses the properties of this entry from the ZIP64 Extended Information - * Extra Field, if present. - * The ZIP64 Extended Information Extra Field is not removed. - * - * @return bool - * @throws ZipException - */ - private function parseZip64ExtraField() - { - if (null === $this->fields) { - return false; - } - $ef = $this->fields->get(ExtraField::ZIP64_HEADER_ID); - if (null === $ef) { - return false; - } - $dataBlockHandle = $ef->getDataBlock(); - $off = 0; - // Read in Uncompressed Size. - $size = $this->getSize(); - if (0xffffffff <= $size) { - assert(0xffffffff === $size); - fseek($dataBlockHandle, $off); - $this->setSize(PackUtil::unpackLongLE(fread($dataBlockHandle, 8))); - $off += 8; - } - // Read in Compressed Size. - $compressedSize = $this->getCompressedSize(); - if (0xffffffff <= $compressedSize) { - assert(0xffffffff === $compressedSize); - fseek($dataBlockHandle, $off); - $this->setCompressedSize(PackUtil::unpackLongLE(fread($dataBlockHandle, 8))); - $off += 8; - } - // Read in Relative Header Offset. - $offset = $this->getOffset(); - if (0xffffffff <= $offset) { - assert(0xffffffff, $offset); - fseek($dataBlockHandle, $off); - $this->setOffset(PackUtil::unpackLongLE(fread($dataBlockHandle, 8))); - //$off += 8; - } - fclose($dataBlockHandle); - return true; } /** @@ -803,7 +663,7 @@ abstract class ZipAbstractEntry implements ZipEntry */ public function getComment() { - return null != $this->comment ? $this->comment : ""; + return null !== $this->comment ? $this->comment : ""; } /** @@ -883,7 +743,11 @@ abstract class ZipAbstractEntry implements ZipEntry if (null !== $encryptionMethod) { $this->setEncryptionMethod($encryptionMethod); } - $this->setEncrypted(!empty($this->password)); + if (!empty($this->password)) { + $this->setEncrypted(true); + } else { + $this->clearEncryption(); + } return $this; } @@ -895,6 +759,34 @@ abstract class ZipAbstractEntry implements ZipEntry return $this->encryptionMethod; } + /** + * Set encryption method + * + * @see ZipFileInterface::ENCRYPTION_METHOD_TRADITIONAL + * @see ZipFileInterface::ENCRYPTION_METHOD_WINZIP_AES_128 + * @see ZipFileInterface::ENCRYPTION_METHOD_WINZIP_AES_192 + * @see ZipFileInterface::ENCRYPTION_METHOD_WINZIP_AES_256 + * + * @param int $encryptionMethod + * @return ZipEntry + * @throws ZipException + */ + public function setEncryptionMethod($encryptionMethod) + { + if (null !== $encryptionMethod) { + if ( + ZipFileInterface::ENCRYPTION_METHOD_TRADITIONAL !== $encryptionMethod + && ZipFileInterface::ENCRYPTION_METHOD_WINZIP_AES_128 !== $encryptionMethod + && ZipFileInterface::ENCRYPTION_METHOD_WINZIP_AES_192 !== $encryptionMethod + && ZipFileInterface::ENCRYPTION_METHOD_WINZIP_AES_256 !== $encryptionMethod + ) { + throw new ZipException('Invalid encryption method'); + } + $this->encryptionMethod = $encryptionMethod; + } + return $this; + } + /** * @return int */ @@ -908,46 +800,23 @@ abstract class ZipAbstractEntry implements ZipEntry * @return ZipEntry * @throws InvalidArgumentException */ - public function setCompressionLevel($compressionLevel = ZipFile::LEVEL_DEFAULT_COMPRESSION) + public function setCompressionLevel($compressionLevel = ZipFileInterface::LEVEL_DEFAULT_COMPRESSION) { - if ($compressionLevel < ZipFile::LEVEL_DEFAULT_COMPRESSION || - $compressionLevel > ZipFile::LEVEL_BEST_COMPRESSION + if ($compressionLevel < ZipFileInterface::LEVEL_DEFAULT_COMPRESSION || + $compressionLevel > ZipFileInterface::LEVEL_BEST_COMPRESSION ) { throw new InvalidArgumentException('Invalid compression level. Minimum level ' . - ZipFile::LEVEL_DEFAULT_COMPRESSION . '. Maximum level ' . ZipFile::LEVEL_BEST_COMPRESSION); + ZipFileInterface::LEVEL_DEFAULT_COMPRESSION . '. Maximum level ' . ZipFileInterface::LEVEL_BEST_COMPRESSION); } $this->compressionLevel = $compressionLevel; return $this; } - /** - * Set encryption method - * - * @see ZipFile::ENCRYPTION_METHOD_TRADITIONAL - * @see ZipFile::ENCRYPTION_METHOD_WINZIP_AES - * - * @param int $encryptionMethod - * @return ZipEntry - * @throws ZipException - */ - public function setEncryptionMethod($encryptionMethod) - { - if ( - ZipFile::ENCRYPTION_METHOD_TRADITIONAL !== $encryptionMethod && - ZipFile::ENCRYPTION_METHOD_WINZIP_AES !== $encryptionMethod - ) { - throw new ZipException('Invalid encryption method'); - } - $this->encryptionMethod = $encryptionMethod; - $this->setEncrypted(true); - return $this; - } - /** * Clone extra fields */ - function __clone() + public function __clone() { - $this->fields = $this->fields !== null ? clone $this->fields : null; + $this->extraFieldsCollection = clone $this->extraFieldsCollection; } -} \ No newline at end of file +} diff --git a/src/PhpZip/Model/Entry/ZipChangesEntry.php b/src/PhpZip/Model/Entry/ZipChangesEntry.php new file mode 100644 index 0000000..205a793 --- /dev/null +++ b/src/PhpZip/Model/Entry/ZipChangesEntry.php @@ -0,0 +1,63 @@ +entry = $entry; + $this->setEntry($entry); + } + + /** + * @return bool + */ + public function isChangedContent() + { + return !( + $this->getCompressionLevel() === $this->entry->getCompressionLevel() && + $this->getMethod() === $this->entry->getMethod() && + $this->isEncrypted() === $this->entry->isEncrypted() && + $this->getEncryptionMethod() === $this->entry->getEncryptionMethod() && + $this->getPassword() === $this->entry->getPassword() + ); + } + + /** + * Returns an string content of the given entry. + * + * @return null|string + * @throws ZipException + */ + public function getEntryContent() + { + return $this->entry->getEntryContent(); + } + + /** + * @return ZipSourceEntry + */ + public function getSourceEntry() + { + return $this->entry; + } +} diff --git a/src/PhpZip/Model/Entry/ZipNewEmptyDirEntry.php b/src/PhpZip/Model/Entry/ZipNewEmptyDirEntry.php deleted file mode 100644 index de5f480..0000000 --- a/src/PhpZip/Model/Entry/ZipNewEmptyDirEntry.php +++ /dev/null @@ -1,26 +0,0 @@ -content = $content; + } + + /** + * Returns an string content of the given entry. + * + * @return null|string + * @throws ZipException + */ + public function getEntryContent() + { + if (is_resource($this->content)) { + return stream_get_contents($this->content, -1, 0); + } + return $this->content; + } /** * Version needed to extract. @@ -32,237 +61,29 @@ abstract class ZipNewEntry extends ZipAbstractEntry { $method = $this->getMethod(); return self::METHOD_WINZIP_AES === $method ? 51 : - (ZipFile::METHOD_BZIP2 === $method ? 46 : - ($this->isZip64ExtensionsRequired() ? 45 : - (ZipFile::METHOD_DEFLATED === $method || $this->isDirectory() ? 20 : 10) + ( + ZipFileInterface::METHOD_BZIP2 === $method ? 46 : + ( + $this->isZip64ExtensionsRequired() ? 45 : + (ZipFileInterface::METHOD_DEFLATED === $method || $this->isDirectory() ? 20 : 10) ) ); } /** - * Write local file header, encryption header, file data and data descriptor to output stream. - * - * @param resource $outputStream - * @throws ZipException + * Clone extra fields */ - public function writeEntry($outputStream) + public function __clone() { - $nameLength = strlen($this->getName()); - $size = $nameLength + strlen($this->getExtra()) + strlen($this->getComment()); - if (0xffff < $size) { - throw new ZipException($this->getName() - . " (the total size of " - . $size - . " bytes for the name, extra fields and comment exceeds the maximum size of " - . 0xffff . " bytes)"); - } - - if (self::UNKNOWN === $this->getPlatform()) { - $this->setPlatform(self::PLATFORM_UNIX); - } - if (self::UNKNOWN === $this->getTime()) { - $this->setTime(time()); - } - $method = $this->getMethod(); - if (self::UNKNOWN === $method) { - $this->setMethod($method = ZipFile::METHOD_DEFLATED); - } - $skipCrc = false; - - $encrypted = $this->isEncrypted(); - $dd = $this->isDataDescriptorRequired(); - // Compose General Purpose Bit Flag. - // See appendix D of PKWARE's ZIP File Format Specification. - $utf8 = true; - $general = ($encrypted ? self::GPBF_ENCRYPTED : 0) - | ($dd ? self::GPBF_DATA_DESCRIPTOR : 0) - | ($utf8 ? self::GPBF_UTF8 : 0); - - $entryContent = $this->getEntryContent(); - - $this->setSize(strlen($entryContent)); - $this->setCrc(crc32($entryContent)); - - if ($encrypted && null === $this->getPassword()) { - throw new ZipException("Can not password from entry " . $this->getName()); - } - - if ( - $encrypted && - ( - self::METHOD_WINZIP_AES === $method || - $this->getEncryptionMethod() === ZipFile::ENCRYPTION_METHOD_WINZIP_AES - ) - ) { - $field = null; - $method = $this->getMethod(); - $keyStrength = 256; // bits - - $compressedSize = $this->getCompressedSize(); - - if (self::METHOD_WINZIP_AES === $method) { - /** - * @var WinZipAesEntryExtraField $field - */ - $field = $this->getExtraField(WinZipAesEntryExtraField::getHeaderId()); - if (null !== $field) { - $method = $field->getMethod(); - if (self::UNKNOWN !== $compressedSize) { - $compressedSize -= $field->getKeyStrength() / 2 // salt value - + 2 // password verification value - + 10; // authentication code - } - $this->setMethod($method); - } - } - if (null === $field) { - $field = new WinZipAesEntryExtraField(); - } - $field->setKeyStrength($keyStrength); - $field->setMethod($method); - $size = $this->getSize(); - if (20 <= $size && ZipFile::METHOD_BZIP2 !== $method) { - $field->setVendorVersion(WinZipAesEntryExtraField::VV_AE_1); - } else { - $field->setVendorVersion(WinZipAesEntryExtraField::VV_AE_2); - $skipCrc = true; - } - $this->addExtraField($field); - if (self::UNKNOWN !== $compressedSize) { - $compressedSize += $field->getKeyStrength() / 2 // salt value - + 2 // password verification value - + 10; // authentication code - $this->setCompressedSize($compressedSize); - } - if ($skipCrc) { - $this->setCrc(0); - } - } - - switch ($method) { - case ZipFile::METHOD_STORED: - break; - case ZipFile::METHOD_DEFLATED: - $entryContent = gzdeflate($entryContent, $this->getCompressionLevel()); - break; - case ZipFile::METHOD_BZIP2: - $compressionLevel = $this->getCompressionLevel() === ZipFile::LEVEL_DEFAULT_COMPRESSION ? - self::LEVEL_DEFAULT_BZIP2_COMPRESSION : - $this->getCompressionLevel(); - $entryContent = bzcompress($entryContent, $compressionLevel); - if (is_int($entryContent)) { - throw new ZipException('Error bzip2 compress. Error code: ' . $entryContent); - } - break; - default: - throw new ZipException($this->getName() . " (unsupported compression method " . $method . ")"); - } - - if ($encrypted) { - if ($this->getEncryptionMethod() === ZipFile::ENCRYPTION_METHOD_WINZIP_AES) { - if ($skipCrc) { - $this->setCrc(0); - } - $this->setMethod(self::METHOD_WINZIP_AES); - - /** - * @var WinZipAesEntryExtraField $field - */ - $field = $this->getExtraField(WinZipAesEntryExtraField::getHeaderId()); - $winZipAesEngine = new WinZipAesEngine($this, $field); - $entryContent = $winZipAesEngine->encrypt($entryContent); - } elseif ($this->getEncryptionMethod() === ZipFile::ENCRYPTION_METHOD_TRADITIONAL) { - $zipCryptoEngine = new TraditionalPkwareEncryptionEngine($this); - $entryContent = $zipCryptoEngine->encrypt($entryContent); - } - } - - $compressedSize = strlen($entryContent); - $this->setCompressedSize($compressedSize); - - $offset = ftell($outputStream); - - // Commit changes. - $this->setGeneralPurposeBitFlags($general); - $this->setOffset($offset); - - $extra = $this->getExtra(); - - // zip align - $padding = 0; - $zipAlign = $this->getCentralDirectory()->getZipAlign(); - $extraLength = strlen($extra); - if ($zipAlign !== null && !$this->isEncrypted() && $this->getMethod() === ZipFile::METHOD_STORED) { - $padding = - ( - $zipAlign - - ( - $offset + - ZipEntry::LOCAL_FILE_HEADER_MIN_LEN + - $nameLength + $extraLength - ) % $zipAlign - ) % $zipAlign; - } - - fwrite( - $outputStream, - pack( - 'VvvvVVVVvv', - // local file header signature 4 bytes (0x04034b50) - self::LOCAL_FILE_HEADER_SIG, - // version needed to extract 2 bytes - $this->getVersionNeededToExtract(), - // general purpose bit flag 2 bytes - $general, - // compression method 2 bytes - $this->getMethod(), - // last mod file time 2 bytes - // last mod file date 2 bytes - $this->getDosTime(), - // crc-32 4 bytes - $dd ? 0 : $this->getCrc(), - // compressed size 4 bytes - $dd ? 0 : $this->getCompressedSize(), - // uncompressed size 4 bytes - $dd ? 0 : $this->getSize(), - // file name length 2 bytes - $nameLength, - // extra field length 2 bytes - $extraLength + $padding - ) - ); - fwrite($outputStream, $this->getName()); - if ($extraLength > 0) { - fwrite($outputStream, $extra); - } - - if ($padding > 0) { - fwrite($outputStream, str_repeat(chr(0), $padding)); - } - - if (null !== $entryContent) { - fwrite($outputStream, $entryContent); - } - - assert(self::UNKNOWN !== $this->getCrc()); - assert(self::UNKNOWN !== $this->getSize()); - if ($this->getGeneralPurposeBitFlag(self::GPBF_DATA_DESCRIPTOR)) { - // data descriptor signature 4 bytes (0x08074b50) - // crc-32 4 bytes - fwrite($outputStream, pack('VV', self::DATA_DESCRIPTOR_SIG, $this->getCrc())); - // compressed size 4 or 8 bytes - // uncompressed size 4 or 8 bytes - if ($this->isZip64ExtensionsRequired()) { - fwrite($outputStream, PackUtil::packLongLE($compressedSize)); - fwrite($outputStream, PackUtil::packLongLE($this->getSize())); - } else { - fwrite($outputStream, pack('VV', $this->getCompressedSize(), $this->getSize())); - } - } elseif ($this->getCompressedSize() != $compressedSize) { - throw new ZipException($this->getName() - . " (expected compressed entry size of " - . $this->getCompressedSize() . " bytes, but is actually " . $compressedSize . " bytes)"); - } + $this->clone = true; + parent::__clone(); } -} \ No newline at end of file + public function __destruct() + { + if (!$this->clone && null !== $this->content && is_resource($this->content)) { + fclose($this->content); + $this->content = null; + } + } +} diff --git a/src/PhpZip/Model/Entry/ZipNewStreamEntry.php b/src/PhpZip/Model/Entry/ZipNewStreamEntry.php deleted file mode 100644 index a8eb518..0000000 --- a/src/PhpZip/Model/Entry/ZipNewStreamEntry.php +++ /dev/null @@ -1,55 +0,0 @@ -stream = $stream; - } - - /** - * Returns an string content of the given entry. - * - * @return null|string - * @throws ZipException - */ - public function getEntryContent() - { - return stream_get_contents($this->stream, -1, 0); - } - - /** - * Release stream resource. - */ - function __destruct() - { - if (null !== $this->stream) { - fclose($this->stream); - $this->stream = null; - } - } -} \ No newline at end of file diff --git a/src/PhpZip/Model/Entry/ZipNewStringEntry.php b/src/PhpZip/Model/Entry/ZipNewStringEntry.php deleted file mode 100644 index d376957..0000000 --- a/src/PhpZip/Model/Entry/ZipNewStringEntry.php +++ /dev/null @@ -1,39 +0,0 @@ -entryContent = $entryContent; - } - - /** - * Returns an string content of the given entry. - * - * @return null|string - * @throws ZipException - */ - public function getEntryContent() - { - return $this->entryContent; - } -} \ No newline at end of file diff --git a/src/PhpZip/Model/Entry/ZipReadEntry.php b/src/PhpZip/Model/Entry/ZipReadEntry.php deleted file mode 100644 index 2363884..0000000 --- a/src/PhpZip/Model/Entry/ZipReadEntry.php +++ /dev/null @@ -1,327 +0,0 @@ -inputStream = $inputStream; - $this->readZipEntry($inputStream); - } - - /** - * @param resource $inputStream - * @throws InvalidArgumentException - */ - private function readZipEntry($inputStream) - { - // central file header signature 4 bytes (0x02014b50) - $fileHeaderSig = unpack('V', fread($inputStream, 4))[1]; - if (CentralDirectory::CENTRAL_FILE_HEADER_SIG !== $fileHeaderSig) { - throw new InvalidArgumentException("Corrupt zip file. Can not read zip entry."); - } - - // version made by 2 bytes - // version needed to extract 2 bytes - // general purpose bit flag 2 bytes - // compression method 2 bytes - // last mod file time 2 bytes - // last mod file date 2 bytes - // crc-32 4 bytes - // compressed size 4 bytes - // uncompressed size 4 bytes - // file name length 2 bytes - // extra field length 2 bytes - // file comment length 2 bytes - // disk number start 2 bytes - // internal file attributes 2 bytes - // external file attributes 4 bytes - // relative offset of local header 4 bytes - $data = unpack( - 'vversionMadeBy/vversionNeededToExtract/vgpbf/vrawMethod/VrawTime/VrawCrc/VrawCompressedSize/' . - 'VrawSize/vfileLength/vextraLength/vcommentLength/VrawInternalAttributes/VrawExternalAttributes/VlfhOff', - fread($inputStream, 42) - ); - - $utf8 = 0 !== ($data['gpbf'] & self::GPBF_UTF8); - if ($utf8) { - $this->charset = "UTF-8"; - } - - // See appendix D of PKWARE's ZIP File Format Specification. - $name = fread($inputStream, $data['fileLength']); - - $this->setName($name); - $this->setVersionNeededToExtract($data['versionNeededToExtract']); - $this->setPlatform($data['versionMadeBy'] >> 8); - $this->setGeneralPurposeBitFlags($data['gpbf']); - $this->setMethod($data['rawMethod']); - $this->setDosTime($data['rawTime']); - $this->setCrc($data['rawCrc']); - $this->setCompressedSize($data['rawCompressedSize']); - $this->setSize($data['rawSize']); - $this->setExternalAttributes($data['rawExternalAttributes']); - $this->setOffset($data['lfhOff']); // must be unmapped! - if (0 < $data['extraLength']) { - $this->setExtra(fread($inputStream, $data['extraLength'])); - } - if (0 < $data['commentLength']) { - $this->setComment(fread($inputStream, $data['commentLength'])); - } - } - - /** - * Returns an string content of the given entry. - * - * @return string - * @throws ZipException - */ - public function getEntryContent() - { - if (null === $this->entryContent) { - if ($this->isDirectory()) { - $this->entryContent = null; - return $this->entryContent; - } - $isEncrypted = $this->isEncrypted(); - $password = $this->getPassword(); - if ($isEncrypted && empty($password)) { - throw new ZipException("Not set password"); - } - - $pos = $this->getOffset(); - assert(self::UNKNOWN !== $pos); - $startPos = $pos = $this->getCentralDirectory()->getEndOfCentralDirectory()->getMapper()->map($pos); - fseek($this->inputStream, $startPos); - - // local file header signature 4 bytes (0x04034b50) - if (self::LOCAL_FILE_HEADER_SIG !== unpack('V', fread($this->inputStream, 4))[1]) { - throw new ZipException($this->getName() . " (expected Local File Header)"); - } - fseek($this->inputStream, $pos + ZipEntry::LOCAL_FILE_HEADER_FILE_NAME_LENGTH_POS); - // file name length 2 bytes - // extra field length 2 bytes - $data = unpack('vfileLength/vextraLength', fread($this->inputStream, 4)); - $pos += ZipEntry::LOCAL_FILE_HEADER_MIN_LEN + $data['fileLength'] + $data['extraLength']; - - assert(self::UNKNOWN !== $this->getCrc()); - - $method = $this->getMethod(); - - fseek($this->inputStream, $pos); - - // Get raw entry content - $content = fread($this->inputStream, $this->getCompressedSize()); - - // Strong Encryption Specification - WinZip AES - if ($this->isEncrypted()) { - if (self::METHOD_WINZIP_AES === $method) { - $winZipAesEngine = new WinZipAesEngine($this); - $content = $winZipAesEngine->decrypt($content); - // Disable redundant CRC-32 check. - $isEncrypted = false; - - /** - * @var WinZipAesEntryExtraField $field - */ - $field = $this->getExtraField(WinZipAesEntryExtraField::getHeaderId()); - $method = $field->getMethod(); - $this->setEncryptionMethod(ZipFile::ENCRYPTION_METHOD_WINZIP_AES); - } else { - // Traditional PKWARE Decryption - $zipCryptoEngine = new TraditionalPkwareEncryptionEngine($this); - $content = $zipCryptoEngine->decrypt($content); - - $this->setEncryptionMethod(ZipFile::ENCRYPTION_METHOD_TRADITIONAL); - } - } - if ($isEncrypted) { - // Check CRC32 in the Local File Header or Data Descriptor. - $localCrc = null; - if ($this->getGeneralPurposeBitFlag(self::GPBF_DATA_DESCRIPTOR)) { - // The CRC32 is in the Data Descriptor after the compressed size. - // Note the Data Descriptor's Signature is optional: - // All newer apps should write it (and so does TrueVFS), - // but older apps might not. - fseek($this->inputStream, $pos + $this->getCompressedSize()); - $localCrc = unpack('V', fread($this->inputStream, 4))[1]; - if (self::DATA_DESCRIPTOR_SIG === $localCrc) { - $localCrc = unpack('V', fread($this->inputStream, 4))[1]; - } - } else { - fseek($this->inputStream, $startPos + 14); - // The CRC32 in the Local File Header. - $localCrc = unpack('V', fread($this->inputStream, 4))[1]; - } - if ($this->getCrc() !== $localCrc) { - throw new Crc32Exception($this->getName(), $this->getCrc(), $localCrc); - } - } - - switch ($method) { - case ZipFile::METHOD_STORED: - break; - case ZipFile::METHOD_DEFLATED: - $content = gzinflate($content); - break; - case ZipFile::METHOD_BZIP2: - if (!extension_loaded('bz2')) { - throw new ZipException('Extension bzip2 not install'); - } - $content = bzdecompress($content); - break; - default: - throw new ZipUnsupportMethod($this->getName() - . " (compression method " - . $method - . " is not supported)"); - } - if ($isEncrypted) { - $localCrc = crc32($content); - if ($this->getCrc() !== $localCrc) { - if ($this->isEncrypted()) { - throw new ZipCryptoException("Wrong password"); - } - throw new Crc32Exception($this->getName(), $this->getCrc(), $localCrc); - } - } - if ($this->getSize() < self::MAX_SIZE_CACHED_CONTENT_IN_MEMORY) { - $this->entryContent = $content; - } else { - $this->entryContent = fopen('php://temp', 'rb'); - fwrite($this->entryContent, $content); - } - return $content; - } - if (is_resource($this->entryContent)) { - return stream_get_contents($this->entryContent, -1, 0); - } - return $this->entryContent; - } - - /** - * Write local file header, encryption header, file data and data descriptor to output stream. - * - * @param resource $outputStream - */ - public function writeEntry($outputStream) - { - $pos = $this->getOffset(); - assert(ZipEntry::UNKNOWN !== $pos); - $pos = $this->getCentralDirectory()->getEndOfCentralDirectory()->getMapper()->map($pos); - $pos += ZipEntry::LOCAL_FILE_HEADER_FILE_NAME_LENGTH_POS; - - $this->setOffset(ftell($outputStream)); - // zip align - $padding = 0; - $zipAlign = $this->getCentralDirectory()->getZipAlign(); - $extra = $this->getExtra(); - $extraLength = strlen($extra); - $nameLength = strlen($this->getName()); - if ($zipAlign !== null && !$this->isEncrypted() && $this->getMethod() === ZipFile::METHOD_STORED) { - $padding = - ( - $zipAlign - - ($this->getOffset() + ZipEntry::LOCAL_FILE_HEADER_MIN_LEN + $nameLength + $extraLength) - % $zipAlign - ) % $zipAlign; - } - $dd = $this->isDataDescriptorRequired(); - - fwrite( - $outputStream, - pack( - 'VvvvVVVVvv', - // local file header signature 4 bytes (0x04034b50) - self::LOCAL_FILE_HEADER_SIG, - // version needed to extract 2 bytes - $this->getVersionNeededToExtract(), - // general purpose bit flag 2 bytes - $this->getGeneralPurposeBitFlags(), - // compression method 2 bytes - $this->getMethod(), - // last mod file time 2 bytes - // last mod file date 2 bytes - $this->getDosTime(), - // crc-32 4 bytes - $dd ? 0 : $this->getCrc(), - // compressed size 4 bytes - $dd ? 0 : $this->getCompressedSize(), - // uncompressed size 4 bytes - $dd ? 0 : $this->getSize(), - $nameLength, - // extra field length 2 bytes - $extraLength + $padding - ) - ); - fwrite($outputStream, $this->getName()); - if ($extraLength > 0) { - fwrite($outputStream, $extra); - } - - if ($padding > 0) { - fwrite($outputStream, str_repeat(chr(0), $padding)); - } - - fseek($this->inputStream, $pos); - $data = unpack('vfileLength/vextraLength', fread($this->inputStream, 4)); - fseek($this->inputStream, $data['fileLength'] + $data['extraLength'], SEEK_CUR); - - $length = $this->getCompressedSize(); - if ($this->getGeneralPurposeBitFlag(ZipEntry::GPBF_DATA_DESCRIPTOR)) { - $length += 12; - if ($this->isZip64ExtensionsRequired()) { - $length += 8; - } - } - stream_copy_to_stream($this->inputStream, $outputStream, $length); - } - - function __destruct() - { - if (null !== $this->entryContent && is_resource($this->entryContent)) { - fclose($this->entryContent); - } - } - -} \ No newline at end of file diff --git a/src/PhpZip/Model/Entry/ZipSourceEntry.php b/src/PhpZip/Model/Entry/ZipSourceEntry.php new file mode 100644 index 0000000..7c43f10 --- /dev/null +++ b/src/PhpZip/Model/Entry/ZipSourceEntry.php @@ -0,0 +1,95 @@ +inputStream = $inputStream; + } + + /** + * @return ZipInputStreamInterface + */ + public function getInputStream() + { + return $this->inputStream; + } + + /** + * Returns an string content of the given entry. + * + * @return string + * @throws ZipException + */ + public function getEntryContent() + { + if (null === $this->entryContent) { + $content = $this->inputStream->readEntryContent($this); + if ($this->getSize() < self::MAX_SIZE_CACHED_CONTENT_IN_MEMORY) { + $this->entryContent = $content; + } else { + $this->entryContent = fopen('php://temp', 'rb'); + fwrite($this->entryContent, $content); + } + return $content; + } + if (is_resource($this->entryContent)) { + return stream_get_contents($this->entryContent, -1, 0); + } + return $this->entryContent; + } + + /** + * Clone extra fields + */ + public function __clone() + { + $this->clone = true; + parent::__clone(); + } + + public function __destruct() + { + if (!$this->clone && null !== $this->entryContent && is_resource($this->entryContent)) { + fclose($this->entryContent); + } + } +} diff --git a/src/PhpZip/Model/ZipEntry.php b/src/PhpZip/Model/ZipEntry.php index b9caf4d..7f80995 100644 --- a/src/PhpZip/Model/ZipEntry.php +++ b/src/PhpZip/Model/ZipEntry.php @@ -1,9 +1,11 @@ zipModel = $zipModel; + } + + /** + * @param string|array $entries + * @return ZipEntryMatcher + */ + public function add($entries) + { + $entries = (array)$entries; + $entries = array_map(function ($entry) { + return $entry instanceof ZipEntry ? $entry->getName() : $entry; + }, $entries); + $this->matches = array_unique( + array_merge( + $this->matches, + array_keys( + array_intersect_key( + $this->zipModel->getEntries(), + array_flip($entries) + ) + ) + ) + ); + return $this; + } + + /** + * @param string $regexp + * @return ZipEntryMatcher + */ + public function match($regexp) + { + array_walk($this->zipModel->getEntries(), function ( + /** @noinspection PhpUnusedParameterInspection */ + $entry, + $entryName + ) use ($regexp) { + if (preg_match($regexp, $entryName)) { + $this->matches[] = $entryName; + } + }); + $this->matches = array_unique($this->matches); + return $this; + } + + /** + * @return ZipEntryMatcher + */ + public function all() + { + $this->matches = array_keys($this->zipModel->getEntries()); + return $this; + } + + /** + * Callable function for all select entries. + * + * Callable function signature: + * function(string $entryName){} + * + * @param callable $callable + */ + public function invoke(callable $callable) + { + if (!empty($this->matches)) { + array_walk($this->matches, function ($entryName) use ($callable) { + call_user_func($callable, $entryName); + }); + } + } + + /** + * @return array + */ + public function getMatches() + { + return $this->matches; + } + + public function delete() + { + array_walk($this->matches, function ($entry) { + $this->zipModel->deleteEntry($entry); + }); + $this->matches = []; + } + + /** + * @param string|null $password + * @param int|null $encryptionMethod + */ + public function setPassword($password, $encryptionMethod = null) + { + array_walk($this->matches, function ($entry) use ($password, $encryptionMethod) { + $entry = $this->zipModel->getEntry($entry); + if (!$entry->isDirectory()) { + $this->zipModel->getEntryForChanges($entry)->setPassword($password, $encryptionMethod); + } + }); + } + + /** + * @param int $encryptionMethod + */ + public function setEncryptionMethod($encryptionMethod) + { + array_walk($this->matches, function ($entry) use ($encryptionMethod) { + $entry = $this->zipModel->getEntry($entry); + if (!$entry->isDirectory()) { + $this->zipModel->getEntryForChanges($entry)->setEncryptionMethod($encryptionMethod); + } + }); + } + + public function disableEncryption() + { + array_walk($this->matches, function ($entry) { + $entry = $this->zipModel->getEntry($entry); + if (!$entry->isDirectory()) { + $entry = $this->zipModel->getEntryForChanges($entry); + $entry->clearEncryption(); + } + }); + } + + /** + * Count elements of an object + * @link http://php.net/manual/en/countable.count.php + * @return int The custom count as an integer. + * + *+ * The return value is cast to an integer. + * @since 5.1.0 + */ + public function count() + { + return count($this->matches); + } +} diff --git a/src/PhpZip/Model/ZipInfo.php b/src/PhpZip/Model/ZipInfo.php index 1dfec17..193087c 100644 --- a/src/PhpZip/Model/ZipInfo.php +++ b/src/PhpZip/Model/ZipInfo.php @@ -1,10 +1,11 @@ 'no compression', + ZipEntry::UNKNOWN => 'unknown', + ZipFileInterface::METHOD_STORED => 'no compression', 1 => 'shrink', 2 => 'reduce level 1', 3 => 'reduce level 2', @@ -94,7 +96,7 @@ class ZipInfo 5 => 'reduce level 4', 6 => 'implode', 7 => 'reserved for Tokenizing compression algorithm', - ZipFile::METHOD_DEFLATED => 'deflate', + ZipFileInterface::METHOD_DEFLATED => 'deflate', 9 => 'deflate64', 10 => 'PKWARE Data Compression Library Imploding (old IBM TERSE)', 11 => 'reserved by PKWARE', @@ -114,72 +116,71 @@ class ZipInfo /** * @var string */ - private $path; - + private $name; /** * @var bool */ private $folder; - /** * @var int */ private $size; - /** * @var int */ private $compressedSize; - /** * @var int */ private $mtime; - /** * @var int|null */ private $ctime; - /** * @var int|null */ private $atime; - /** * @var bool */ private $encrypted; - /** * @var string|null */ private $comment; - /** * @var int */ private $crc; - /** * @var string */ - private $method; - + private $methodName; + /** + * @var int + */ + private $compressionMethod; /** * @var string */ private $platform; - /** * @var int */ private $version; - /** * @var string */ private $attributes; + /** + * @var int|null + */ + private $encryptionMethod; + /** + * @var int|null + */ + private $compressionLevel; /** * ZipInfo constructor. @@ -192,16 +193,17 @@ class ZipInfo $atime = null; $ctime = null; - $field = $entry->getExtraField(NtfsExtraField::getHeaderId()); + $field = $entry->getExtraFieldsCollection()->get(NtfsExtraField::getHeaderId()); if (null !== $field && $field instanceof NtfsExtraField) { /** * @var NtfsExtraField $field */ $atime = $field->getAtime(); $ctime = $field->getCtime(); + $mtime = $field->getMtime(); } - $this->path = $entry->getName(); + $this->name = $entry->getName(); $this->folder = $entry->isDirectory(); $this->size = $entry->getSize(); $this->compressedSize = $entry->getCompressedSize(); @@ -209,18 +211,22 @@ class ZipInfo $this->ctime = $ctime; $this->atime = $atime; $this->encrypted = $entry->isEncrypted(); + $this->encryptionMethod = $entry->getEncryptionMethod(); $this->comment = $entry->getComment(); $this->crc = $entry->getCrc(); - $this->method = self::getMethodName($entry); + $this->compressionMethod = self::getMethodId($entry); + $this->methodName = self::getEntryMethodName($entry); $this->platform = self::getPlatformName($entry); $this->version = $entry->getVersionNeededToExtract(); + $this->compressionLevel = $entry->getCompressionLevel(); $attributes = str_repeat(" ", 12); $externalAttributes = $entry->getExternalAttributes(); $xattr = (($externalAttributes >> 16) & 0xFFFF); switch ($entry->getPlatform()) { case self::MADE_BY_MS_DOS: - /** @noinspection PhpMissingBreakStatementInspection */ + // no break + /** @noinspection PhpMissingBreakStatementInspection */ case self::MADE_BY_WINDOWS_NTFS: if ($entry->getPlatform() != self::MADE_BY_MS_DOS || ($xattr & 0700) != @@ -237,11 +243,12 @@ class ZipInfo if ($xattr & 0x10) { $attributes[0] = 'd'; $attributes[3] = 'x'; - } else + } else { $attributes[0] = '-'; - if ($xattr & 0x08) + } + if ($xattr & 0x08) { $attributes[0] = 'V'; - else { + } else { $ext = strtolower(pathinfo($entry->getName(), PATHINFO_EXTENSION)); if (in_array($ext, ["com", "exe", "btm", "cmd", "bat"])) { $attributes[3] = 'x'; @@ -250,6 +257,7 @@ class ZipInfo break; } /* else: fall through! */ + // no break default: /* assume Unix-like */ switch ($xattr & self::UNX_IFMT) { case self::UNX_IFDIR: @@ -284,33 +292,59 @@ class ZipInfo $attributes[5] = ($xattr & self::UNX_IWGRP) ? 'w' : '-'; $attributes[8] = ($xattr & self::UNX_IWOTH) ? 'w' : '-'; - if ($xattr & self::UNX_IXUSR) + if ($xattr & self::UNX_IXUSR) { $attributes[3] = ($xattr & self::UNX_ISUID) ? 's' : 'x'; - else - $attributes[3] = ($xattr & self::UNX_ISUID) ? 'S' : '-'; /* S==undefined */ - if ($xattr & self::UNX_IXGRP) - $attributes[6] = ($xattr & self::UNX_ISGID) ? 's' : 'x'; /* == UNX_ENFMT */ - else - $attributes[6] = ($xattr & self::UNX_ISGID) ? 'S' : '-'; /* SunOS 4.1.x */ - if ($xattr & self::UNX_IXOTH) - $attributes[9] = ($xattr & self::UNX_ISVTX) ? 't' : 'x'; /* "sticky bit" */ - else - $attributes[9] = ($xattr & self::UNX_ISVTX) ? 'T' : '-'; /* T==undefined */ + } else { + $attributes[3] = ($xattr & self::UNX_ISUID) ? 'S' : '-'; + } /* S==undefined */ + if ($xattr & self::UNX_IXGRP) { + $attributes[6] = ($xattr & self::UNX_ISGID) ? 's' : 'x'; + } /* == UNX_ENFMT */ + else { + $attributes[6] = ($xattr & self::UNX_ISGID) ? 'S' : '-'; + } /* SunOS 4.1.x */ + if ($xattr & self::UNX_IXOTH) { + $attributes[9] = ($xattr & self::UNX_ISVTX) ? 't' : 'x'; + } /* "sticky bit" */ + else { + $attributes[9] = ($xattr & self::UNX_ISVTX) ? 'T' : '-'; + } /* T==undefined */ } $this->attributes = trim($attributes); } + /** + * @param ZipEntry $entry + * @return int + */ + private static function getMethodId(ZipEntry $entry) + { + $method = $entry->getMethod(); + if ($entry->isEncrypted()) { + if ($entry->getMethod() === ZipEntry::METHOD_WINZIP_AES) { + $field = $entry->getExtraFieldsCollection()->get(WinZipAesEntryExtraField::getHeaderId()); + if (null !== $field) { + /** + * @var WinZipAesEntryExtraField $field + */ + $method = $field->getMethod(); + } + } + } + return $method; + } + /** * @param ZipEntry $entry * @return string */ - public static function getMethodName(ZipEntry $entry) + private static function getEntryMethodName(ZipEntry $entry) { $return = ''; if ($entry->isEncrypted()) { if ($entry->getMethod() === ZipEntry::METHOD_WINZIP_AES) { - $field = $entry->getExtraField(WinZipAesEntryExtraField::getHeaderId()); $return = ucfirst(self::$valuesCompressionMethod[$entry->getMethod()]); + $field = $entry->getExtraFieldsCollection()->get(WinZipAesEntryExtraField::getHeaderId()); if (null !== $field) { /** * @var WinZipAesEntryExtraField $field @@ -348,34 +382,20 @@ class ZipInfo } /** - * @return array + * @return string */ - public function toArray() + public function getName() { - return [ - 'path' => $this->getPath(), - 'folder' => $this->isFolder(), - 'size' => $this->getSize(), - 'compressed_size' => $this->getCompressedSize(), - 'modified' => $this->getMtime(), - 'created' => $this->getCtime(), - 'accessed' => $this->getAtime(), - 'attributes' => $this->getAttributes(), - 'encrypted' => $this->isEncrypted(), - 'comment' => $this->getComment(), - 'crc' => $this->getCrc(), - 'method' => $this->getMethod(), - 'platform' => $this->getPlatform(), - 'version' => $this->getVersion() - ]; + return $this->name; } /** * @return string + * @deprecated use \PhpZip\Model\ZipInfo::getName() */ public function getPath() { - return $this->path; + return $this->getName(); } /** @@ -426,6 +446,14 @@ class ZipInfo return $this->atime; } + /** + * @return string + */ + public function getAttributes() + { + return $this->attributes; + } + /** * @return boolean */ @@ -452,10 +480,19 @@ class ZipInfo /** * @return string + * @deprecated use \PhpZip\Model\ZipInfo::getMethodName() */ public function getMethod() { - return $this->method; + return $this->getMethodName(); + } + + /** + * @return string + */ + public function getMethodName() + { + return $this->methodName; } /** @@ -475,35 +512,76 @@ class ZipInfo } /** - * @return string + * @return int|null */ - public function getAttributes() + public function getEncryptionMethod() { - return $this->attributes; + return $this->encryptionMethod; + } + + /** + * @return int|null + */ + public function getCompressionLevel() + { + return $this->compressionLevel; + } + + /** + * @return int + */ + public function getCompressionMethod() + { + return $this->compressionMethod; + } + + /** + * @return array + */ + public function toArray() + { + return [ + 'name' => $this->getName(), + 'path' => $this->getName(), // deprecated + 'folder' => $this->isFolder(), + 'size' => $this->getSize(), + 'compressed_size' => $this->getCompressedSize(), + 'modified' => $this->getMtime(), + 'created' => $this->getCtime(), + 'accessed' => $this->getAtime(), + 'attributes' => $this->getAttributes(), + 'encrypted' => $this->isEncrypted(), + 'encryption_method' => $this->getEncryptionMethod(), + 'comment' => $this->getComment(), + 'crc' => $this->getCrc(), + 'method' => $this->getMethodName(), // deprecated + 'method_name' => $this->getMethodName(), + 'compression_method' => $this->getCompressionMethod(), + 'platform' => $this->getPlatform(), + 'version' => $this->getVersion() + ]; } /** * @return string */ - function __toString() + public function __toString() { - return 'ZipInfo {' - . 'Path="' . $this->getPath() . '", ' - . ($this->isFolder() ? 'Folder, ' : '') - . 'Size=' . FilesUtil::humanSize($this->getSize()) - . ', Compressed size=' . FilesUtil::humanSize($this->getCompressedSize()) - . ', Modified time=' . date(DATE_W3C, $this->getMtime()) . ', ' - . ($this->getCtime() !== null ? 'Created time=' . date(DATE_W3C, $this->getCtime()) . ', ' : '') - . ($this->getAtime() !== null ? 'Accessed time=' . date(DATE_W3C, $this->getAtime()) . ', ' : '') - . ($this->isEncrypted() ? 'Encrypted, ' : '') - . (!empty($this->comment) ? 'Comment="' . $this->getComment() . '", ' : '') - . (!empty($this->crc) ? 'Crc=0x' . dechex($this->getCrc()) . ', ' : '') - . 'Method="' . $this->getMethod() . '", ' - . 'Attributes="' . $this->getAttributes() . '", ' - . 'Platform="' . $this->getPlatform() . '", ' - . 'Version=' . $this->getVersion() - . '}'; + return __CLASS__ . ' {' + . 'Name="' . $this->getName() . '", ' + . ($this->isFolder() ? 'Folder, ' : '') + . 'Size="' . FilesUtil::humanSize($this->getSize()).'"' + . ', Compressed size="' . FilesUtil::humanSize($this->getCompressedSize()).'"' + . ', Modified time="' . date(DATE_W3C, $this->getMtime()) . '", ' + . ($this->getCtime() !== null ? 'Created time="' . date(DATE_W3C, $this->getCtime()) . '", ' : '') + . ($this->getAtime() !== null ? 'Accessed time="' . date(DATE_W3C, $this->getAtime()) . '", ' : '') + . ($this->isEncrypted() ? 'Encrypted, ' : '') + . (!empty($this->comment) ? 'Comment="' . $this->getComment() . '", ' : '') + . (!empty($this->crc) ? 'Crc=0x' . dechex($this->getCrc()) . ', ' : '') + . 'Method name="' . $this->getMethodName() . '", ' + . 'Attributes="' . $this->getAttributes() . '", ' + . 'Platform="' . $this->getPlatform() . '", ' + . 'Version=' . $this->getVersion() + . '}'; } - - -} \ No newline at end of file +} diff --git a/src/PhpZip/Model/ZipModel.php b/src/PhpZip/Model/ZipModel.php new file mode 100644 index 0000000..c5866b2 --- /dev/null +++ b/src/PhpZip/Model/ZipModel.php @@ -0,0 +1,341 @@ +inputEntries = $entries; + $model->outEntries = $entries; + $model->archiveComment = $endOfCentralDirectory->getComment(); + $model->zip64 = $endOfCentralDirectory->isZip64(); + return $model; + } + + /** + * @return null|string + */ + public function getArchiveComment() + { + if ($this->archiveCommentChanged) { + return $this->archiveCommentChanges; + } + return $this->archiveComment; + } + + /** + * @param string $comment + * @throws InvalidArgumentException + */ + public function setArchiveComment($comment) + { + if (null !== $comment && strlen($comment) !== 0) { + $comment = (string)$comment; + $length = strlen($comment); + if (0x0000 > $length || $length > 0xffff) { + throw new InvalidArgumentException('Length comment out of range'); + } + } + if ($comment !== $this->archiveComment) { + $this->archiveCommentChanges = $comment; + $this->archiveCommentChanged = true; + } else { + $this->archiveCommentChanged = false; + } + } + + /** + * Specify a password for extracting files. + * + * @param null|string $password + */ + public function setReadPassword($password) + { + foreach ($this->inputEntries as $entry) { + if ($entry->isEncrypted()) { + $entry->setPassword($password); + } + } + } + + /** + * @param string $entryName + * @param string $password + * @throws ZipNotFoundEntry + */ + public function setReadPasswordEntry($entryName, $password) + { + if (!isset($this->inputEntries[$entryName])) { + throw new ZipNotFoundEntry('Not found entry ' . $entryName); + } + if ($this->inputEntries[$entryName]->isEncrypted()) { + $this->inputEntries[$entryName]->setPassword($password); + } + } + + /** + * @return int|null + */ + public function getZipAlign() + { + return $this->zipAlign; + } + + /** + * @param int|null $zipAlign + */ + public function setZipAlign($zipAlign) + { + $this->zipAlign = $zipAlign === null ? null : (int)$zipAlign; + } + + /** + * @return bool + */ + public function isZipAlign() + { + return $this->zipAlign != null; + } + + /** + * @param null|string $writePassword + */ + public function setWritePassword($writePassword) + { + $this->matcher()->all()->setPassword($writePassword); + } + + /** + * Remove password + */ + public function removePassword() + { + $this->matcher()->all()->setPassword(null); + } + + public function removePasswordEntry($entryName) + { + $this->matcher()->add($entryName)->setPassword(null); + } + + /** + * @return bool + */ + public function isArchiveCommentChanged() + { + return $this->archiveCommentChanged; + } + + /** + * @param string|ZipEntry $old + * @param string|ZipEntry $new + * @throws InvalidArgumentException + * @throws ZipNotFoundEntry + */ + public function renameEntry($old, $new) + { + $old = $old instanceof ZipEntry ? $old->getName() : (string)$old; + $new = $new instanceof ZipEntry ? $new->getName() : (string)$new; + + if (isset($this->outEntries[$new])) { + throw new InvalidArgumentException("New entry name " . $new . ' is exists.'); + } + + $entry = $this->getEntryForChanges($old); + $entry->setName($new); + $this->deleteEntry($old); + $this->addEntry($entry); + } + + /** + * @param string|ZipEntry $entry + * @return ZipChangesEntry|ZipEntry + */ + public function getEntryForChanges($entry) + { + $entry = $this->getEntry($entry); + if ($entry instanceof ZipSourceEntry) { + $entry = new ZipChangesEntry($entry); + $this->addEntry($entry); + } + return $entry; + } + + /** + * @param string|ZipEntry $entryName + * @return ZipEntry + * @throws ZipNotFoundEntry + */ + public function getEntry($entryName) + { + $entryName = $entryName instanceof ZipEntry ? $entryName->getName() : (string)$entryName; + if (isset($this->outEntries[$entryName])) { + return $this->outEntries[$entryName]; + } + throw new ZipNotFoundEntry('Zip entry ' . $entryName . ' not found'); + } + + /** + * @param string|ZipEntry $entry + * @return bool + */ + public function deleteEntry($entry) + { + $entry = $entry instanceof ZipEntry ? $entry->getName() : (string)$entry; + if (isset($this->outEntries[$entry])) { + unset($this->outEntries[$entry]); + return true; + } + return false; + } + + /** + * @param ZipEntry $entry + */ + public function addEntry(ZipEntry $entry) + { + $this->outEntries[$entry->getName()] = $entry; + } + + /** + * Get all entries with changes. + * + * @return ZipEntry[] + */ + public function &getEntries() + { + return $this->outEntries; + } + + /** + * @param string|ZipEntry $entryName + * @return bool + */ + public function hasEntry($entryName) + { + $entryName = $entryName instanceof ZipEntry ? $entryName->getName() : (string)$entryName; + return isset($this->outEntries[$entryName]); + } + + /** + * Delete all entries. + */ + public function deleteAll() + { + $this->outEntries = []; + } + + /** + * Count elements of an object + * @link http://php.net/manual/en/countable.count.php + * @return int The custom count as an integer. + *
+ *+ * The return value is cast to an integer. + * @since 5.1.0 + */ + public function count() + { + return sizeof($this->outEntries); + } + + /** + * Undo all changes done in the archive + */ + public function unchangeAll() + { + $this->outEntries = $this->inputEntries; + $this->unchangeArchiveComment(); + } + + /** + * Undo change archive comment + */ + public function unchangeArchiveComment() + { + $this->archiveCommentChanges = null; + $this->archiveCommentChanged = false; + } + + /** + * Revert all changes done to an entry with the given name. + * + * @param string|ZipEntry $entry Entry name or ZipEntry + * @return bool + */ + public function unchangeEntry($entry) + { + $entry = $entry instanceof ZipEntry ? $entry->getName() : (string)$entry; + if (isset($this->outEntries[$entry]) && isset($this->inputEntries[$entry])) { + $this->outEntries[$entry] = $this->inputEntries[$entry]; + return true; + } + return false; + } + + /** + * @param int $encryptionMethod + * @throws ZipException + */ + public function setEncryptionMethod($encryptionMethod = ZipFileInterface::ENCRYPTION_METHOD_WINZIP_AES_256) + { + $this->matcher()->all()->setEncryptionMethod($encryptionMethod); + } + + /** + * @return ZipEntryMatcher + */ + public function matcher() + { + return new ZipEntryMatcher($this); + } +} diff --git a/src/PhpZip/Stream/ResponseStream.php b/src/PhpZip/Stream/ResponseStream.php new file mode 100644 index 0000000..172de1e --- /dev/null +++ b/src/PhpZip/Stream/ResponseStream.php @@ -0,0 +1,298 @@ + [ + 'r' => true, 'w+' => true, 'r+' => true, 'x+' => true, 'c+' => true, + 'rb' => true, 'w+b' => true, 'r+b' => true, 'x+b' => true, + 'c+b' => true, 'rt' => true, 'w+t' => true, 'r+t' => true, + 'x+t' => true, 'c+t' => true, 'a+' => true, + ], + 'write' => [ + 'w' => true, 'w+' => true, 'rw' => true, 'r+' => true, 'x+' => true, + 'c+' => true, 'wb' => true, 'w+b' => true, 'r+b' => true, + 'x+b' => true, 'c+b' => true, 'w+t' => true, 'r+t' => true, + 'x+t' => true, 'c+t' => true, 'a' => true, 'a+' => true, + ], + ]; + /** + * @var resource + */ + private $stream; + /** + * @var int + */ + private $size; + /** + * @var bool + */ + private $seekable; + /** + * @var bool + */ + private $readable; + /** + * @var bool + */ + private $writable; + /** + * @var array|mixed|null + */ + private $uri; + + /** + * @param resource $stream Stream resource to wrap. + * @throws \InvalidArgumentException if the stream is not a stream resource + */ + public function __construct($stream) + { + if (!is_resource($stream)) { + throw new \InvalidArgumentException('Stream must be a resource'); + } + $this->stream = $stream; + $meta = stream_get_meta_data($this->stream); + $this->seekable = $meta['seekable']; + $this->readable = isset(self::$readWriteHash['read'][$meta['mode']]); + $this->writable = isset(self::$readWriteHash['write'][$meta['mode']]); + $this->uri = $this->getMetadata('uri'); + } + + /** + * Get stream metadata as an associative array or retrieve a specific key. + * + * The keys returned are identical to the keys returned from PHP's + * stream_get_meta_data() function. + * + * @link http://php.net/manual/en/function.stream-get-meta-data.php + * @param string $key Specific metadata to retrieve. + * @return array|mixed|null Returns an associative array if no key is + * provided. Returns a specific key value if a key is provided and the + * value is found, or null if the key is not found. + */ + public function getMetadata($key = null) + { + if (!$this->stream) { + return $key ? null : []; + } + $meta = stream_get_meta_data($this->stream); + return isset($meta[$key]) ? $meta[$key] : null; + } + + /** + * Reads all data from the stream into a string, from the beginning to end. + * + * This method MUST attempt to seek to the beginning of the stream before + * reading data and read the stream until the end is reached. + * + * Warning: This could attempt to load a large amount of data into memory. + * + * This method MUST NOT raise an exception in order to conform with PHP's + * string casting operations. + * + * @see http://php.net/manual/en/language.oop5.magic.php#object.tostring + * @return string + */ + public function __toString() + { + if (!$this->stream) { + return ''; + } + $this->rewind(); + return (string)stream_get_contents($this->stream); + } + + /** + * Seek to the beginning of the stream. + * + * If the stream is not seekable, this method will raise an exception; + * otherwise, it will perform a seek(0). + * + * @see seek() + * @link http://www.php.net/manual/en/function.fseek.php + * @throws \RuntimeException on failure. + */ + public function rewind() + { + $this->seekable && rewind($this->stream); + } + + /** + * Get the size of the stream if known. + * + * @return int|null Returns the size in bytes if known, or null if unknown. + */ + public function getSize() + { + if ($this->size !== null) { + return $this->size; + } + if (!$this->stream) { + return null; + } + // Clear the stat cache if the stream has a URI + if ($this->uri) { + clearstatcache(true, $this->uri); + } + $stats = fstat($this->stream); + if (isset($stats['size'])) { + $this->size = $stats['size']; + return $this->size; + } + return null; + } + + /** + * Returns the current position of the file read/write pointer + * + * @return int Position of the file pointer + * @throws \RuntimeException on error. + */ + public function tell() + { + return $this->stream ? ftell($this->stream) : false; + } + + /** + * Returns true if the stream is at the end of the stream. + * + * @return bool + */ + public function eof() + { + return !$this->stream || feof($this->stream); + } + + /** + * Returns whether or not the stream is seekable. + * + * @return bool + */ + public function isSeekable() + { + return $this->seekable; + } + + /** + * Seek to a position in the stream. + * + * @link http://www.php.net/manual/en/function.fseek.php + * @param int $offset Stream offset + * @param int $whence Specifies how the cursor position will be calculated + * based on the seek offset. Valid values are identical to the built-in + * PHP $whence values for `fseek()`. SEEK_SET: Set position equal to + * offset bytes SEEK_CUR: Set position to current location plus offset + * SEEK_END: Set position to end-of-stream plus offset. + * @throws \RuntimeException on failure. + */ + public function seek($offset, $whence = SEEK_SET) + { + $this->seekable && fseek($this->stream, $offset, $whence); + } + + /** + * Returns whether or not the stream is writable. + * + * @return bool + */ + public function isWritable() + { + return $this->writable; + } + + /** + * Write data to the stream. + * + * @param string $string The string that is to be written. + * @return int Returns the number of bytes written to the stream. + * @throws \RuntimeException on failure. + */ + public function write($string) + { + $this->size = null; + return $this->writable ? fwrite($this->stream, $string) : false; + } + + /** + * Returns whether or not the stream is readable. + * + * @return bool + */ + public function isReadable() + { + return $this->readable; + } + + /** + * Read data from the stream. + * + * @param int $length Read up to $length bytes from the object and return + * them. Fewer than $length bytes may be returned if underlying stream + * call returns fewer bytes. + * @return string Returns the data read from the stream, or an empty string + * if no bytes are available. + * @throws \RuntimeException if an error occurs. + */ + public function read($length) + { + return $this->readable ? fread($this->stream, $length) : ""; + } + + /** + * Returns the remaining contents in a string + * + * @return string + * @throws \RuntimeException if unable to read or an error occurs while + * reading. + */ + public function getContents() + { + return $this->stream ? stream_get_contents($this->stream) : ''; + } + + /** + * Closes the stream when the destructed + */ + public function __destruct() + { + $this->close(); + } + + /** + * Closes the stream and any underlying resources. + * + * @return void + */ + public function close() + { + if (is_resource($this->stream)) { + fclose($this->stream); + } + $this->detach(); + } + + /** + * Separates any underlying resources from the stream. + * + * After the stream has been detached, the stream is in an unusable state. + * + * @return resource|null Underlying PHP stream, if any + */ + public function detach() + { + $result = $this->stream; + $this->stream = $this->size = $this->uri = null; + $this->readable = $this->writable = $this->seekable = false; + return $result; + } +} diff --git a/src/PhpZip/Stream/ZipInputStream.php b/src/PhpZip/Stream/ZipInputStream.php new file mode 100644 index 0000000..2510f43 --- /dev/null +++ b/src/PhpZip/Stream/ZipInputStream.php @@ -0,0 +1,532 @@ +in = $in; + $this->mapper = new PositionMapper(); + } + + /** + * @return ZipModel + */ + public function readZip() + { + $this->checkZipFileSignature(); + $endOfCentralDirectory = $this->readEndOfCentralDirectory(); + $entries = $this->mountCentralDirectory($endOfCentralDirectory); + $this->zipModel = ZipModel::newSourceModel($entries, $endOfCentralDirectory); + return $this->zipModel; + } + + /** + * Check zip file signature + * + * @throws ZipException if this not .ZIP file. + */ + protected function checkZipFileSignature() + { + rewind($this->in); + // Constraint: A ZIP file must start with a Local File Header + // or a (ZIP64) End Of Central Directory Record if it's empty. + $signatureBytes = fread($this->in, 4); + if (strlen($signatureBytes) < 4) { + throw new ZipException("Invalid zip file."); + } + $signature = unpack('V', $signatureBytes)[1]; + if ( + ZipEntry::LOCAL_FILE_HEADER_SIG !== $signature + && EndOfCentralDirectory::ZIP64_END_OF_CENTRAL_DIRECTORY_RECORD_SIG !== $signature + && EndOfCentralDirectory::END_OF_CENTRAL_DIRECTORY_RECORD_SIG !== $signature + ) { + throw new ZipException("Expected Local File Header or (ZIP64) End Of Central Directory Record! Signature: " . $signature); + } + } + + /** + * @return EndOfCentralDirectory + * @throws ZipException + */ + protected function readEndOfCentralDirectory() + { + $comment = null; + // Search for End of central directory record. + $stats = fstat($this->in); + $size = $stats['size']; + $max = $size - EndOfCentralDirectory::END_OF_CENTRAL_DIRECTORY_RECORD_MIN_LEN; + $min = $max >= 0xffff ? $max - 0xffff : 0; + for ($endOfCentralDirRecordPos = $max; $endOfCentralDirRecordPos >= $min; $endOfCentralDirRecordPos--) { + fseek($this->in, $endOfCentralDirRecordPos, SEEK_SET); + // end of central dir signature 4 bytes (0x06054b50) + if (EndOfCentralDirectory::END_OF_CENTRAL_DIRECTORY_RECORD_SIG !== unpack('V', fread($this->in, 4))[1]) { + continue; + } + + // number of this disk - 2 bytes + // number of the disk with the start of the + // central directory - 2 bytes + // total number of entries in the central + // directory on this disk - 2 bytes + // total number of entries in the central + // directory - 2 bytes + // size of the central directory - 4 bytes + // offset of start of central directory with + // respect to the starting disk number - 4 bytes + // ZIP file comment length - 2 bytes + $data = unpack( + 'vdiskNo/vcdDiskNo/vcdEntriesDisk/vcdEntries/VcdSize/VcdPos/vcommentLength', + fread($this->in, 18) + ); + + if (0 !== $data['diskNo'] || 0 !== $data['cdDiskNo'] || $data['cdEntriesDisk'] !== $data['cdEntries']) { + throw new ZipException( + "ZIP file spanning/splitting is not supported!" + ); + } + // .ZIP file comment (variable size) + if (0 < $data['commentLength']) { + $comment = fread($this->in, $data['commentLength']); + } + $this->preamble = $endOfCentralDirRecordPos; + $this->postamble = $size - ftell($this->in); + + // Check for ZIP64 End Of Central Directory Locator. + $endOfCentralDirLocatorPos = $endOfCentralDirRecordPos - EndOfCentralDirectory::ZIP64_END_OF_CENTRAL_DIRECTORY_LOCATOR_LEN; + + fseek($this->in, $endOfCentralDirLocatorPos, SEEK_SET); + // zip64 end of central dir locator + // signature 4 bytes (0x07064b50) + if ( + 0 > $endOfCentralDirLocatorPos || + ftell($this->in) === $size || + EndOfCentralDirectory::ZIP64_END_OF_CENTRAL_DIRECTORY_LOCATOR_SIG !== unpack('V', fread($this->in, 4))[1] + ) { + // Seek and check first CFH, probably requiring an offset mapper. + $offset = $endOfCentralDirRecordPos - $data['cdSize']; + fseek($this->in, $offset, SEEK_SET); + $offset -= $data['cdPos']; + if (0 !== $offset) { + $this->mapper = new OffsetPositionMapper($offset); + } + $entryCount = $data['cdEntries']; + return new EndOfCentralDirectory($entryCount, $comment); + } + + // number of the disk with the + // start of the zip64 end of + // central directory 4 bytes + $zip64EndOfCentralDirectoryRecordDisk = unpack('V', fread($this->in, 4))[1]; + // relative offset of the zip64 + // end of central directory record 8 bytes + $zip64EndOfCentralDirectoryRecordPos = PackUtil::unpackLongLE(fread($this->in, 8)); + // total number of disks 4 bytes + $totalDisks = unpack('V', fread($this->in, 4))[1]; + if (0 !== $zip64EndOfCentralDirectoryRecordDisk || 1 !== $totalDisks) { + throw new ZipException("ZIP file spanning/splitting is not supported!"); + } + fseek($this->in, $zip64EndOfCentralDirectoryRecordPos, SEEK_SET); + // zip64 end of central dir + // signature 4 bytes (0x06064b50) + $zip64EndOfCentralDirSig = unpack('V', fread($this->in, 4))[1]; + if (EndOfCentralDirectory::ZIP64_END_OF_CENTRAL_DIRECTORY_RECORD_SIG !== $zip64EndOfCentralDirSig) { + throw new ZipException("Expected ZIP64 End Of Central Directory Record!"); + } + // size of zip64 end of central + // directory record 8 bytes + // version made by 2 bytes + // version needed to extract 2 bytes + fseek($this->in, 12, SEEK_CUR); + // number of this disk 4 bytes + $diskNo = unpack('V', fread($this->in, 4))[1]; + // number of the disk with the + // start of the central directory 4 bytes + $cdDiskNo = unpack('V', fread($this->in, 4))[1]; + // total number of entries in the + // central directory on this disk 8 bytes + $cdEntriesDisk = PackUtil::unpackLongLE(fread($this->in, 8)); + // total number of entries in the + // central directory 8 bytes + $cdEntries = PackUtil::unpackLongLE(fread($this->in, 8)); + if (0 !== $diskNo || 0 !== $cdDiskNo || $cdEntriesDisk !== $cdEntries) { + throw new ZipException("ZIP file spanning/splitting is not supported!"); + } + if ($cdEntries < 0 || 0x7fffffff < $cdEntries) { + throw new ZipException("Total Number Of Entries In The Central Directory out of range!"); + } + // size of the central directory 8 bytes + fseek($this->in, 8, SEEK_CUR); + // offset of start of central + // directory with respect to + // the starting disk number 8 bytes + $cdPos = PackUtil::unpackLongLE(fread($this->in, 8)); + // zip64 extensible data sector (variable size) + fseek($this->in, $cdPos, SEEK_SET); + $this->preamble = $zip64EndOfCentralDirectoryRecordPos; + $entryCount = $cdEntries; + $zip64 = true; + return new EndOfCentralDirectory($entryCount, $comment, $zip64); + } + // Start recovering file entries from min. + $this->preamble = $min; + $this->postamble = $size - $min; + return new EndOfCentralDirectory(0, $comment); + } + + /** + * Reads the central directory from the given seekable byte channel + * and populates the internal tables with ZipEntry instances. + * + * The ZipEntry's will know all data that can be obtained from the + * central directory alone, but not the data that requires the local + * file header or additional data to be read. + * + * @param EndOfCentralDirectory $endOfCentralDirectory + * @return ZipEntry[] + * @throws ZipException + */ + protected function mountCentralDirectory(EndOfCentralDirectory $endOfCentralDirectory) + { + $numEntries = $endOfCentralDirectory->getEntryCount(); + $entries = []; + + for (; $numEntries > 0; $numEntries--) { + $entry = $this->readEntry(); + // Re-load virtual offset after ZIP64 Extended Information + // Extra Field may have been parsed, map it to the real + // offset and conditionally update the preamble size from it. + $lfhOff = $this->mapper->map($entry->getOffset()); + if ($lfhOff < $this->preamble) { + $this->preamble = $lfhOff; + } + $entries[$entry->getName()] = $entry; + } + + if (0 !== $numEntries % 0x10000) { + throw new ZipException("Expected " . abs($numEntries) . + ($numEntries > 0 ? " more" : " less") . + " entries in the Central Directory!"); + } + + if ($this->preamble + $this->postamble >= fstat($this->in)['size']) { + assert(0 === $numEntries); + $this->checkZipFileSignature(); + } + + return $entries; + } + + /** + * @return ZipEntry + * @throws InvalidArgumentException + */ + public function readEntry() + { + // central file header signature 4 bytes (0x02014b50) + $fileHeaderSig = unpack('V', fread($this->in, 4))[1]; + if (ZipOutputStreamInterface::CENTRAL_FILE_HEADER_SIG !== $fileHeaderSig) { + throw new InvalidArgumentException("Corrupt zip file. Can not read zip entry."); + } + + // version made by 2 bytes + // version needed to extract 2 bytes + // general purpose bit flag 2 bytes + // compression method 2 bytes + // last mod file time 2 bytes + // last mod file date 2 bytes + // crc-32 4 bytes + // compressed size 4 bytes + // uncompressed size 4 bytes + // file name length 2 bytes + // extra field length 2 bytes + // file comment length 2 bytes + // disk number start 2 bytes + // internal file attributes 2 bytes + // external file attributes 4 bytes + // relative offset of local header 4 bytes + $data = unpack( + 'vversionMadeBy/vversionNeededToExtract/vgpbf/' . + 'vrawMethod/VrawTime/VrawCrc/VrawCompressedSize/' . + 'VrawSize/vfileLength/vextraLength/vcommentLength/' . + 'VrawInternalAttributes/VrawExternalAttributes/VlfhOff', + fread($this->in, 42) + ); + +// $utf8 = 0 !== ($data['gpbf'] & self::GPBF_UTF8); + + // See appendix D of PKWARE's ZIP File Format Specification. + $name = fread($this->in, $data['fileLength']); + + $entry = new ZipSourceEntry($this); + $entry->setName($name); + $entry->setVersionNeededToExtract($data['versionNeededToExtract']); + $entry->setPlatform($data['versionMadeBy'] >> 8); + $entry->setMethod($data['rawMethod']); + $entry->setGeneralPurposeBitFlags($data['gpbf']); + $entry->setDosTime($data['rawTime']); + $entry->setCrc($data['rawCrc']); + $entry->setCompressedSize($data['rawCompressedSize']); + $entry->setSize($data['rawSize']); + $entry->setExternalAttributes($data['rawExternalAttributes']); + $entry->setOffset($data['lfhOff']); // must be unmapped! + if (0 < $data['extraLength']) { + $entry->setExtra(fread($this->in, $data['extraLength'])); + } + if (0 < $data['commentLength']) { + $entry->setComment(fread($this->in, $data['commentLength'])); + } + return $entry; + } + + /** + * @param ZipEntry $entry + * @return string + * @throws ZipException + */ + public function readEntryContent(ZipEntry $entry) + { + if ($entry->isDirectory()) { + return null; + } + if (!($entry instanceof ZipSourceEntry)) { + throw new InvalidArgumentException('entry must be ' . ZipSourceEntry::class); + } + $isEncrypted = $entry->isEncrypted(); + if ($isEncrypted && null === $entry->getPassword()) { + throw new ZipException("Can not password from entry " . $entry->getName()); + } + + $pos = $entry->getOffset(); + assert(ZipEntry::UNKNOWN !== $pos); + $startPos = $pos = $this->mapper->map($pos); + fseek($this->in, $startPos); + + // local file header signature 4 bytes (0x04034b50) + if (ZipEntry::LOCAL_FILE_HEADER_SIG !== unpack('V', fread($this->in, 4))[1]) { + throw new ZipException($entry->getName() . " (expected Local File Header)"); + } + fseek($this->in, $pos + ZipEntry::LOCAL_FILE_HEADER_FILE_NAME_LENGTH_POS); + // file name length 2 bytes + // extra field length 2 bytes + $data = unpack('vfileLength/vextraLength', fread($this->in, 4)); + $pos += ZipEntry::LOCAL_FILE_HEADER_MIN_LEN + $data['fileLength'] + $data['extraLength']; + + assert(ZipEntry::UNKNOWN !== $entry->getCrc()); + + $method = $entry->getMethod(); + + fseek($this->in, $pos); + + // Get raw entry content + $compressedSize = $entry->getCompressedSize(); + if ($compressedSize > 0) { + $content = fread($this->in, $compressedSize); + } else { + $content = ''; + } + + $skipCheckCrc = false; + if ($isEncrypted) { + if (ZipEntry::METHOD_WINZIP_AES === $method) { + // Strong Encryption Specification - WinZip AES + $winZipAesEngine = new WinZipAesEngine($entry); + $content = $winZipAesEngine->decrypt($content); + /** + * @var WinZipAesEntryExtraField $field + */ + $field = $entry->getExtraFieldsCollection()->get(WinZipAesEntryExtraField::getHeaderId()); + $method = $field->getMethod(); + $entry->setEncryptionMethod($field->getEncryptionMethod()); + $skipCheckCrc = true; + } else { + // Traditional PKWARE Decryption + $zipCryptoEngine = new TraditionalPkwareEncryptionEngine($entry); + $content = $zipCryptoEngine->decrypt($content); + $entry->setEncryptionMethod(ZipFileInterface::ENCRYPTION_METHOD_TRADITIONAL); + } + + if (!$skipCheckCrc) { + // Check CRC32 in the Local File Header or Data Descriptor. + $localCrc = null; + if ($entry->getGeneralPurposeBitFlag(ZipEntry::GPBF_DATA_DESCRIPTOR)) { + // The CRC32 is in the Data Descriptor after the compressed size. + // Note the Data Descriptor's Signature is optional: + // All newer apps should write it (and so does TrueVFS), + // but older apps might not. + fseek($this->in, $pos + $compressedSize); + $localCrc = unpack('V', fread($this->in, 4))[1]; + if (ZipEntry::DATA_DESCRIPTOR_SIG === $localCrc) { + $localCrc = unpack('V', fread($this->in, 4))[1]; + } + } else { + fseek($this->in, $startPos + 14); + // The CRC32 in the Local File Header. + $localCrc = sprintf('%u', fread($this->in, 4)[1]); + } + if ($entry->getCrc() != $localCrc) { + throw new Crc32Exception($entry->getName(), $entry->getCrc(), $localCrc); + } + } + } + + switch ($method) { + case ZipFileInterface::METHOD_STORED: + break; + case ZipFileInterface::METHOD_DEFLATED: + $content = gzinflate($content); + break; + case ZipFileInterface::METHOD_BZIP2: + if (!extension_loaded('bz2')) { + throw new ZipException('Extension bzip2 not install'); + } + $content = bzdecompress($content); + break; + default: + throw new ZipUnsupportMethod($entry->getName() . + " (compression method " . $method . " is not supported)"); + } + if (!$skipCheckCrc) { + $localCrc = sprintf('%u', crc32($content)); + if ($entry->getCrc() != $localCrc) { + if ($isEncrypted) { + throw new ZipCryptoException("Wrong password"); + } + throw new Crc32Exception($entry->getName(), $entry->getCrc(), $localCrc); + } + } + return $content; + } + + /** + * @return resource + */ + public function getStream() + { + return $this->in; + } + + /** + * @param ZipEntry $entry + * @param ZipOutputStreamInterface $out + */ + public function copyEntry(ZipEntry $entry, ZipOutputStreamInterface $out) + { + $pos = $entry->getOffset(); + assert(ZipEntry::UNKNOWN !== $pos); + $pos = $this->mapper->map($pos); + + $extraLength = strlen($entry->getExtra()); + $nameLength = strlen($entry->getName()); + + $length = ZipEntry::LOCAL_FILE_HEADER_MIN_LEN + $extraLength + $nameLength; + + $padding = 0; + if ($this->zipModel->isZipAlign() && !$entry->isEncrypted() && $entry->getMethod() === ZipFileInterface::METHOD_STORED) { + $padding = + ( + $this->zipModel->getZipAlign() - + (ftell($out->getStream()) + $length) % $this->zipModel->getZipAlign() + ) % $this->zipModel->getZipAlign(); + } + + fseek($this->in, $pos, SEEK_SET); + if ($padding > 0) { + stream_copy_to_stream($this->in, $out->getStream(), ZipEntry::LOCAL_FILE_HEADER_MIN_LEN - 2); + fwrite($out->getStream(), pack('v', $extraLength + $padding)); + fseek($this->in, 2, SEEK_CUR); + stream_copy_to_stream($this->in, $out->getStream(), $nameLength + $extraLength); + fwrite($out->getStream(), str_repeat(chr(0), $padding)); + } else { + stream_copy_to_stream($this->in, $out->getStream(), $length); + } + $this->copyEntryData($entry, $out); + if ($entry->getGeneralPurposeBitFlag(ZipEntry::GPBF_DATA_DESCRIPTOR)) { + $length = 12; + if ($entry->isZip64ExtensionsRequired()) { + $length += 8; + } + stream_copy_to_stream($this->in, $out->getStream(), $length); + } + } + + /** + * @param ZipEntry $entry + * @param ZipOutputStreamInterface $out + */ + public function copyEntryData(ZipEntry $entry, ZipOutputStreamInterface $out) + { + $position = $entry->getOffset() + ZipEntry::LOCAL_FILE_HEADER_MIN_LEN + + strlen($entry->getName()) + strlen($entry->getExtra()); + $length = $entry->getCompressedSize(); + fseek($this->in, $position, SEEK_SET); + stream_copy_to_stream($this->in, $out->getStream(), $length); + } + + public function __destruct() + { + $this->close(); + } + + public function close() + { + if ($this->in != null) { + fclose($this->in); + $this->in = null; + } + } +} diff --git a/src/PhpZip/Stream/ZipInputStreamInterface.php b/src/PhpZip/Stream/ZipInputStreamInterface.php new file mode 100644 index 0000000..d5e98a1 --- /dev/null +++ b/src/PhpZip/Stream/ZipInputStreamInterface.php @@ -0,0 +1,50 @@ +out = $out; + $this->zipModel = $zipModel; + } + + public function writeZip() + { + $entries = $this->zipModel->getEntries(); + $outPosEntries = []; + foreach ($entries as $entry) { + $outPosEntries[] = new OutputOffsetEntry(ftell($this->out), $entry); + $this->writeEntry($entry); + } + $centralDirectoryOffset = ftell($this->out); + foreach ($outPosEntries as $outputEntry) { + $this->writeCentralDirectoryHeader($outputEntry); + } + $this->writeEndOfCentralDirectoryRecord($centralDirectoryOffset); + } + + /** + * @param ZipEntry $entry + * @throws ZipException + */ + public function writeEntry(ZipEntry $entry) + { + if ($entry instanceof ZipSourceEntry) { + $entry->getInputStream()->copyEntry($entry, $this); + return; + } + + $entryContent = $this->entryCommitChangesAndReturnContent($entry); + + $offset = ftell($this->out); + $compressedSize = $entry->getCompressedSize(); + + $extra = $entry->getExtra(); + + $nameLength = strlen($entry->getName()); + $extraLength = strlen($extra); + $size = $nameLength + $extraLength; + if (0xffff < $size) { + throw new ZipException( + $entry->getName() . " (the total size of " . $size . + " bytes for the name, extra fields and comment " . + "exceeds the maximum size of " . 0xffff . " bytes)" + ); + } + + // zip align + $padding = 0; + if ($this->zipModel->isZipAlign() && !$entry->isEncrypted() && $entry->getMethod() === ZipFileInterface::METHOD_STORED) { + $padding = + ( + $this->zipModel->getZipAlign() - + ( + $offset + ZipEntry::LOCAL_FILE_HEADER_MIN_LEN + $nameLength + $extraLength + ) % $this->zipModel->getZipAlign() + ) % $this->zipModel->getZipAlign(); + } + + $dd = $entry->isDataDescriptorRequired(); + + fwrite( + $this->out, + pack( + 'VvvvVVVVvv', + // local file header signature 4 bytes (0x04034b50) + ZipEntry::LOCAL_FILE_HEADER_SIG, + // version needed to extract 2 bytes + $entry->getVersionNeededToExtract(), + // general purpose bit flag 2 bytes + $entry->getGeneralPurposeBitFlags(), + // compression method 2 bytes + $entry->getMethod(), + // last mod file time 2 bytes + // last mod file date 2 bytes + $entry->getDosTime(), + // crc-32 4 bytes + $dd ? 0 : $entry->getCrc(), + // compressed size 4 bytes + $dd ? 0 : $entry->getCompressedSize(), + // uncompressed size 4 bytes + $dd ? 0 : $entry->getSize(), + // file name length 2 bytes + $nameLength, + // extra field length 2 bytes + $extraLength + $padding + ) + ); + fwrite($this->out, $entry->getName()); + if ($extraLength > 0) { + fwrite($this->out, $extra); + } + + if ($padding > 0) { + fwrite($this->out, str_repeat(chr(0), $padding)); + } + + if ($entry instanceof ZipChangesEntry && !$entry->isChangedContent()) { + $entry->getSourceEntry()->getInputStream()->copyEntryData($entry->getSourceEntry(), $this); + } elseif (null !== $entryContent) { + fwrite($this->out, $entryContent); + } + + assert(ZipEntry::UNKNOWN !== $entry->getCrc()); + assert(ZipEntry::UNKNOWN !== $entry->getSize()); + if ($entry->getGeneralPurposeBitFlag(ZipEntry::GPBF_DATA_DESCRIPTOR)) { + // data descriptor signature 4 bytes (0x08074b50) + // crc-32 4 bytes + fwrite($this->out, pack('VV', ZipEntry::DATA_DESCRIPTOR_SIG, $entry->getCrc())); + // compressed size 4 or 8 bytes + // uncompressed size 4 or 8 bytes + if ($entry->isZip64ExtensionsRequired()) { + fwrite($this->out, PackUtil::packLongLE($compressedSize)); + fwrite($this->out, PackUtil::packLongLE($entry->getSize())); + } else { + fwrite($this->out, pack('VV', $entry->getCompressedSize(), $entry->getSize())); + } + } elseif ($entry->getCompressedSize() != $compressedSize) { + throw new ZipException( + $entry->getName() . " (expected compressed entry size of " + . $entry->getCompressedSize() . " bytes, " . + "but is actually " . $compressedSize . " bytes)" + ); + } + } + + /** + * @param ZipEntry $entry + * @return null|string + * @throws ZipException + */ + protected function entryCommitChangesAndReturnContent(ZipEntry $entry) + { + if (ZipEntry::UNKNOWN === $entry->getPlatform()) { + $entry->setPlatform(ZipEntry::PLATFORM_UNIX); + } + if (ZipEntry::UNKNOWN === $entry->getTime()) { + $entry->setTime(time()); + } + $method = $entry->getMethod(); + + $encrypted = $entry->isEncrypted(); + // See appendix D of PKWARE's ZIP File Format Specification. + $utf8 = true; + + if ($encrypted && null === $entry->getPassword()) { + throw new ZipException("Can not password from entry " . $entry->getName()); + } + + // Compose General Purpose Bit Flag. + $general = ($encrypted ? ZipEntry::GPBF_ENCRYPTED : 0) + | ($entry->isDataDescriptorRequired() ? ZipEntry::GPBF_DATA_DESCRIPTOR : 0) + | ($utf8 ? ZipEntry::GPBF_UTF8 : 0); + + $skipCrc = false; + $entryContent = null; + $extraFieldsCollection = $entry->getExtraFieldsCollection(); + if (!($entry instanceof ZipChangesEntry && !$entry->isChangedContent())) { + $entryContent = $entry->getEntryContent(); + + if ($entryContent !== null) { + $entry->setSize(strlen($entryContent)); + $entry->setCrc(crc32($entryContent)); + + if ( + $encrypted && + ( + ZipEntry::METHOD_WINZIP_AES === $method || + $entry->getEncryptionMethod() === ZipFileInterface::ENCRYPTION_METHOD_WINZIP_AES_128 || + $entry->getEncryptionMethod() === ZipFileInterface::ENCRYPTION_METHOD_WINZIP_AES_192 || + $entry->getEncryptionMethod() === ZipFileInterface::ENCRYPTION_METHOD_WINZIP_AES_256 + ) + ) { + $field = null; + $method = $entry->getMethod(); + $keyStrength = WinZipAesEntryExtraField::getKeyStrangeFromEncryptionMethod($entry->getEncryptionMethod()); // size bits + + $compressedSize = $entry->getCompressedSize(); + + if (ZipEntry::METHOD_WINZIP_AES === $method) { + /** + * @var WinZipAesEntryExtraField $field + */ + $field = $extraFieldsCollection->get(WinZipAesEntryExtraField::getHeaderId()); + if (null !== $field) { + $method = $field->getMethod(); + if (ZipEntry::UNKNOWN !== $compressedSize) { + $compressedSize -= $field->getKeyStrength() / 2 // salt value + + 2 // password verification value + + 10; // authentication code + } + $entry->setMethod($method); + } + } + if (null === $field) { + $field = ExtraFieldsFactory::createWinZipAesEntryExtra(); + } + $field->setKeyStrength($keyStrength); + $field->setMethod($method); + $size = $entry->getSize(); + if (20 <= $size && ZipFileInterface::METHOD_BZIP2 !== $method) { + $field->setVendorVersion(WinZipAesEntryExtraField::VV_AE_1); + } else { + $field->setVendorVersion(WinZipAesEntryExtraField::VV_AE_2); + $skipCrc = true; + } + $extraFieldsCollection->add($field); + if (ZipEntry::UNKNOWN !== $compressedSize) { + $compressedSize += $field->getKeyStrength() / 2 // salt value + + 2 // password verification value + + 10; // authentication code + $entry->setCompressedSize($compressedSize); + } + if ($skipCrc) { + $entry->setCrc(0); + } + } + + switch ($method) { + case ZipFileInterface::METHOD_STORED: + break; + + case ZipFileInterface::METHOD_DEFLATED: + $entryContent = gzdeflate($entryContent, $entry->getCompressionLevel()); + break; + + case ZipFileInterface::METHOD_BZIP2: + $compressionLevel = $entry->getCompressionLevel() === ZipFileInterface::LEVEL_DEFAULT_COMPRESSION ? + ZipEntry::LEVEL_DEFAULT_BZIP2_COMPRESSION : + $entry->getCompressionLevel(); + $entryContent = bzcompress($entryContent, $compressionLevel); + if (is_int($entryContent)) { + throw new ZipException('Error bzip2 compress. Error code: ' . $entryContent); + } + break; + + case ZipEntry::UNKNOWN: + $entryContent = $this->determineBestCompressionMethod($entry, $entryContent); + $method = $entry->getMethod(); + break; + + default: + throw new ZipException($entry->getName() . " (unsupported compression method " . $method . ")"); + } + + if (ZipFileInterface::METHOD_DEFLATED === $method) { + $bit1 = false; + $bit2 = false; + switch ($entry->getCompressionLevel()) { + case ZipFileInterface::LEVEL_BEST_COMPRESSION: + $bit1 = true; + break; + + case ZipFileInterface::LEVEL_FAST: + $bit2 = true; + break; + + case ZipFileInterface::LEVEL_SUPER_FAST: + $bit1 = true; + $bit2 = true; + break; + } + + $general |= ($bit1 ? ZipEntry::GPBF_COMPRESSION_FLAG1 : 0); + $general |= ($bit2 ? ZipEntry::GPBF_COMPRESSION_FLAG2 : 0); + } + + if ($encrypted) { + if ( + $entry->getEncryptionMethod() === ZipFileInterface::ENCRYPTION_METHOD_WINZIP_AES_128 || + $entry->getEncryptionMethod() === ZipFileInterface::ENCRYPTION_METHOD_WINZIP_AES_192 || + $entry->getEncryptionMethod() === ZipFileInterface::ENCRYPTION_METHOD_WINZIP_AES_256 + ) { + if ($skipCrc) { + $entry->setCrc(0); + } + $entry->setMethod(ZipEntry::METHOD_WINZIP_AES); + + $winZipAesEngine = new WinZipAesEngine($entry); + $entryContent = $winZipAesEngine->encrypt($entryContent); + } elseif ($entry->getEncryptionMethod() === ZipFileInterface::ENCRYPTION_METHOD_TRADITIONAL) { + $zipCryptoEngine = new TraditionalPkwareEncryptionEngine($entry); + $entryContent = $zipCryptoEngine->encrypt($entryContent); + } + } + + $compressedSize = strlen($entryContent); + $entry->setCompressedSize($compressedSize); + } + } + + // Commit changes. + $entry->setGeneralPurposeBitFlags($general); + + if ($entry->isZip64ExtensionsRequired()) { + $extraFieldsCollection->add(ExtraFieldsFactory::createZip64Extra($entry)); + } elseif ($extraFieldsCollection->has(Zip64ExtraField::getHeaderId())) { + $extraFieldsCollection->remove(Zip64ExtraField::getHeaderId()); + } + return $entryContent; + } + + /** + * @param ZipEntry $entry + * @param string $content + * @return string + */ + protected function determineBestCompressionMethod(ZipEntry $entry, $content) + { + if (null !== $content) { + $entryContent = gzdeflate($content, $entry->getCompressionLevel()); + if (strlen($entryContent) < strlen($content)) { + $entry->setMethod(ZipFileInterface::METHOD_DEFLATED); + return $entryContent; + } + $entry->setMethod(ZipFileInterface::METHOD_STORED); + } + return $content; + } + + /** + * Writes a Central File Header record. + * + * @param OutputOffsetEntry $outEntry + * @throws RuntimeException + * @internal param OutPosEntry $entry + */ + protected function writeCentralDirectoryHeader(OutputOffsetEntry $outEntry) + { + $entry = $outEntry->getEntry(); + $compressedSize = $entry->getCompressedSize(); + $size = $entry->getSize(); + // This test MUST NOT include the CRC-32 because VV_AE_2 sets it to + // UNKNOWN! + if (ZipEntry::UNKNOWN === ($compressedSize | $size)) { + throw new RuntimeException("invalid entry"); + } + $extra = $entry->getExtra(); + $extraSize = strlen($extra); + + $commentLength = strlen($entry->getComment()); + fwrite( + $this->out, + pack( + 'VvvvvVVVVvvvvvVV', + // central file header signature 4 bytes (0x02014b50) + self::CENTRAL_FILE_HEADER_SIG, + // version made by 2 bytes + ($entry->getPlatform() << 8) | 63, + // version needed to extract 2 bytes + $entry->getVersionNeededToExtract(), + // general purpose bit flag 2 bytes + $entry->getGeneralPurposeBitFlags(), + // compression method 2 bytes + $entry->getMethod(), + // last mod file datetime 4 bytes + $entry->getDosTime(), + // crc-32 4 bytes + $entry->getCrc(), + // compressed size 4 bytes + $entry->getCompressedSize(), + // uncompressed size 4 bytes + $entry->getSize(), + // file name length 2 bytes + strlen($entry->getName()), + // extra field length 2 bytes + $extraSize, + // file comment length 2 bytes + $commentLength, + // disk number start 2 bytes + 0, + // internal file attributes 2 bytes + 0, + // external file attributes 4 bytes + $entry->getExternalAttributes(), + // relative offset of local header 4 bytes + $outEntry->getOffset() + ) + ); + // file name (variable size) + fwrite($this->out, $entry->getName()); + if (0 < $extraSize) { + // extra field (variable size) + fwrite($this->out, $extra); + } + if (0 < $commentLength) { + // file comment (variable size) + fwrite($this->out, $entry->getComment()); + } + } + + protected function writeEndOfCentralDirectoryRecord($centralDirectoryOffset) + { + $centralDirectoryEntriesCount = count($this->zipModel); + $position = ftell($this->out); + $centralDirectorySize = $position - $centralDirectoryOffset; + $centralDirectoryEntriesZip64 = $centralDirectoryEntriesCount > 0xffff; + $centralDirectorySizeZip64 = $centralDirectorySize > 0xffffffff; + $centralDirectoryOffsetZip64 = $centralDirectoryOffset > 0xffffffff; + $centralDirectoryEntries16 = $centralDirectoryEntriesZip64 ? 0xffff : (int)$centralDirectoryEntriesCount; + $centralDirectorySize32 = $centralDirectorySizeZip64 ? 0xffffffff : $centralDirectorySize; + $centralDirectoryOffset32 = $centralDirectoryOffsetZip64 ? 0xffffffff : $centralDirectoryOffset; + $zip64 // ZIP64 extensions? + = $centralDirectoryEntriesZip64 + || $centralDirectorySizeZip64 + || $centralDirectoryOffsetZip64; + if ($zip64) { + // [zip64 end of central directory record] + // relative offset of the zip64 end of central directory record + $zip64EndOfCentralDirectoryOffset = $position; + // zip64 end of central dir + // signature 4 bytes (0x06064b50) + fwrite($this->out, pack('V', EndOfCentralDirectory::ZIP64_END_OF_CENTRAL_DIRECTORY_RECORD_SIG)); + // size of zip64 end of central + // directory record 8 bytes + fwrite($this->out, PackUtil::packLongLE(EndOfCentralDirectory::ZIP64_END_OF_CENTRAL_DIRECTORY_RECORD_MIN_LEN - 12)); + // version made by 2 bytes + // version needed to extract 2 bytes + // due to potential use of BZIP2 compression + // number of this disk 4 bytes + // number of the disk with the + // start of the central directory 4 bytes + fwrite($this->out, pack('vvVV', 63, 46, 0, 0)); + // total number of entries in the + // central directory on this disk 8 bytes + fwrite($this->out, PackUtil::packLongLE($centralDirectoryEntriesCount)); + // total number of entries in the + // central directory 8 bytes + fwrite($this->out, PackUtil::packLongLE($centralDirectoryEntriesCount)); + // size of the central directory 8 bytes + fwrite($this->out, PackUtil::packLongLE($centralDirectorySize)); + // offset of start of central + // directory with respect to + // the starting disk number 8 bytes + fwrite($this->out, PackUtil::packLongLE($centralDirectoryOffset)); + // zip64 extensible data sector (variable size) + + // [zip64 end of central directory locator] + // signature 4 bytes (0x07064b50) + // number of the disk with the + // start of the zip64 end of + // central directory 4 bytes + fwrite($this->out, pack('VV', EndOfCentralDirectory::ZIP64_END_OF_CENTRAL_DIRECTORY_LOCATOR_SIG, 0)); + // relative offset of the zip64 + // end of central directory record 8 bytes + fwrite($this->out, PackUtil::packLongLE($zip64EndOfCentralDirectoryOffset)); + // total number of disks 4 bytes + fwrite($this->out, pack('V', 1)); + } + $comment = $this->zipModel->getArchiveComment(); + $commentLength = strlen($comment); + fwrite( + $this->out, + pack( + 'VvvvvVVv', + // end of central dir signature 4 bytes (0x06054b50) + EndOfCentralDirectory::END_OF_CENTRAL_DIRECTORY_RECORD_SIG, + // number of this disk 2 bytes + 0, + // number of the disk with the + // start of the central directory 2 bytes + 0, + // total number of entries in the + // central directory on this disk 2 bytes + $centralDirectoryEntries16, + // total number of entries in + // the central directory 2 bytes + $centralDirectoryEntries16, + // size of the central directory 4 bytes + $centralDirectorySize32, + // offset of start of central + // directory with respect to + // the starting disk number 4 bytes + $centralDirectoryOffset32, + // .ZIP file comment length 2 bytes + $commentLength + ) + ); + if ($commentLength > 0) { + // .ZIP file comment (variable size) + fwrite($this->out, $comment); + } + } + + /** + * @return resource + */ + public function getStream() + { + return $this->out; + } +} diff --git a/src/PhpZip/Stream/ZipOutputStreamInterface.php b/src/PhpZip/Stream/ZipOutputStreamInterface.php new file mode 100644 index 0000000..57c397e --- /dev/null +++ b/src/PhpZip/Stream/ZipOutputStreamInterface.php @@ -0,0 +1,29 @@ +> 11) & 0x1f, // hour - ($dosTime >> 5) & 0x3f, // minute - 2 * ($dosTime & 0x1f), // second - ($dosTime >> 21) & 0x0f, // month + ($dosTime >> 5) & 0x3f, // minute + 2 * ($dosTime & 0x1f), // second + ($dosTime >> 21) & 0x0f, // month ($dosTime >> 16) & 0x1f, // day 1980 + (($dosTime >> 25) & 0x7f) // year ); @@ -74,4 +75,4 @@ class DateTimeConverter $date['mday'] << 16 | $date['hours'] << 11 | $date['minutes'] << 5 | $date['seconds'] >> 1); } -} \ No newline at end of file +} diff --git a/src/PhpZip/Util/FilesUtil.php b/src/PhpZip/Util/FilesUtil.php index 9192c6d..29e8688 100644 --- a/src/PhpZip/Util/FilesUtil.php +++ b/src/PhpZip/Util/FilesUtil.php @@ -1,4 +1,5 @@ 0 && !$escaping) { $regexPattern .= ')'; $inCurrent--; - } else if ($escaping) + } elseif ($escaping) { $regexPattern = "\\}"; - else + } else { $regexPattern = "}"; + } $escaping = false; break; case ',': if ($inCurrent > 0 && !$escaping) { $regexPattern .= '|'; - } else if ($escaping) + } elseif ($escaping) { $regexPattern .= "\\,"; - else + } else { $regexPattern = ","; + } break; default: $escaping = false; @@ -211,12 +214,15 @@ class FilesUtil */ public static function humanSize($size, $unit = null) { - if (($unit === null && $size >= 1 << 30) || $unit === "GB") + if (($unit === null && $size >= 1 << 30) || $unit === "GB") { return number_format($size / (1 << 30), 2) . "GB"; - if (($unit === null && $size >= 1 << 20) || $unit === "MB") + } + if (($unit === null && $size >= 1 << 20) || $unit === "MB") { return number_format($size / (1 << 20), 2) . "MB"; - if (($unit === null && $size >= 1 << 10) || $unit === "KB") + } + if (($unit === null && $size >= 1 << 10) || $unit === "KB") { return number_format($size / (1 << 10), 2) . "KB"; + } return number_format($size) . " bytes"; } -} \ No newline at end of file +} diff --git a/src/PhpZip/Util/Iterator/IgnoreFilesFilterIterator.php b/src/PhpZip/Util/Iterator/IgnoreFilesFilterIterator.php index a3b9c9d..40e8fe0 100644 --- a/src/PhpZip/Util/Iterator/IgnoreFilesFilterIterator.php +++ b/src/PhpZip/Util/Iterator/IgnoreFilesFilterIterator.php @@ -1,4 +1,5 @@ ignoreFiles as $ignoreFile) { // handler dir and sub dir if ($fileInfo->isDir() - && $ignoreFile[strlen($ignoreFile) - 1] === '/' + && StringUtil::endsWith($ignoreFile, '/') && StringUtil::endsWith($pathname, substr($ignoreFile, 0, -1)) ) { return false; @@ -57,4 +58,4 @@ class IgnoreFilesFilterIterator extends \FilterIterator } return true; } -} \ No newline at end of file +} diff --git a/src/PhpZip/Util/Iterator/IgnoreFilesRecursiveFilterIterator.php b/src/PhpZip/Util/Iterator/IgnoreFilesRecursiveFilterIterator.php index 131ee3f..7781576 100644 --- a/src/PhpZip/Util/Iterator/IgnoreFilesRecursiveFilterIterator.php +++ b/src/PhpZip/Util/Iterator/IgnoreFilesRecursiveFilterIterator.php @@ -1,4 +1,5 @@ getInnerIterator()->getChildren(), $this->ignoreFiles); } -} \ No newline at end of file +} diff --git a/src/PhpZip/Util/PackUtil.php b/src/PhpZip/Util/PackUtil.php index 1ef10d0..b68a0c8 100644 --- a/src/PhpZip/Util/PackUtil.php +++ b/src/PhpZip/Util/PackUtil.php @@ -1,4 +1,5 @@ = 0) { - return current(unpack('P', $value)); + return unpack('P', $value)[1]; } $unpack = unpack('Va/Vb', $value); return $unpack['a'] + ($unpack['b'] << 32); } -} \ No newline at end of file + /** + * Cast to signed int 32-bit + * + * @param int $int + * @return int + */ + public static function toSignedInt32($int) + { + $int = $int & 0xffffffff; + if (PHP_INT_SIZE === 8 && ($int & 0x80000000)) { + return $int - 0x100000000; + } + return $int; + } +} diff --git a/src/PhpZip/Util/StringUtil.php b/src/PhpZip/Util/StringUtil.php index c596adf..0b75040 100644 --- a/src/PhpZip/Util/StringUtil.php +++ b/src/PhpZip/Util/StringUtil.php @@ -1,4 +1,5 @@ = 0 - && strpos($haystack, $needle, $temp) !== false); + && strpos($haystack, $needle, $temp) !== false); } -} \ No newline at end of file +} diff --git a/src/PhpZip/ZipFile.php b/src/PhpZip/ZipFile.php index 59e9b6b..fc6b9d9 100644 --- a/src/PhpZip/ZipFile.php +++ b/src/PhpZip/ZipFile.php @@ -1,17 +1,23 @@ 'application/epub+zip' ]; + /** + * Input seekable input stream. + * + * @var ZipInputStreamInterface + */ + protected $inputStream; + /** + * @var ZipModel + */ + protected $zipModel; + /** * ZipFile constructor. */ public function __construct() { - $this->centralDirectory = new CentralDirectory(); + $this->zipModel = new ZipModel(); } /** * Open zip archive from file * * @param string $filename - * @return ZipFile + * @return ZipFileInterface * @throws InvalidArgumentException if file doesn't exists. * @throws ZipException if can't open file. */ @@ -129,7 +111,7 @@ class ZipFile implements \Countable, \ArrayAccess, \Iterator * Open zip archive from raw string data. * * @param string $data - * @return ZipFile + * @return ZipFileInterface * @throws InvalidArgumentException if data not available. * @throws ZipException if can't open temp stream. */ @@ -151,7 +133,7 @@ class ZipFile implements \Countable, \ArrayAccess, \Iterator * Open zip archive from stream resource * * @param resource $handle - * @return ZipFile + * @return ZipFileInterface * @throws InvalidArgumentException Invalid stream resource * or resource cannot seekable stream */ @@ -160,44 +142,36 @@ class ZipFile implements \Countable, \ArrayAccess, \Iterator if (!is_resource($handle)) { throw new InvalidArgumentException("Invalid stream resource."); } + $type = get_resource_type($handle); + if ('stream' !== $type) { + throw new InvalidArgumentException("Invalid resource type - $type."); + } $meta = stream_get_meta_data($handle); + if ('dir' === $meta['stream_type']) { + throw new InvalidArgumentException("Invalid stream type - {$meta['stream_type']}."); + } if (!$meta['seekable']) { throw new InvalidArgumentException("Resource cannot seekable stream."); } - $this->inputStream = $handle; - $this->centralDirectory = new CentralDirectory(); - $this->centralDirectory->mountCentralDirectory($this->inputStream); + $this->inputStream = new ZipInputStream($handle); + $this->zipModel = $this->inputStream->readZip(); return $this; } - /** - * @return int Returns the number of entries in this ZIP file. - */ - public function count() - { - return sizeof($this->centralDirectory->getEntries()); - } - /** * @return string[] Returns the list files. */ public function getListFiles() { - return array_keys($this->centralDirectory->getEntries()); + return array_keys($this->zipModel->getEntries()); } /** - * Check whether the directory entry. - * Returns true if and only if this ZIP entry represents a directory entry - * (i.e. end with '/'). - * - * @param string $entryName - * @return bool - * @throws ZipNotFoundEntry + * @return int Returns the number of entries in this ZIP file. */ - public function isDirectory($entryName) + public function count() { - return $this->centralDirectory->getEntry($entryName)->isDirectory(); + return $this->zipModel->count(); } /** @@ -207,34 +181,34 @@ class ZipFile implements \Countable, \ArrayAccess, \Iterator */ public function getArchiveComment() { - return $this->centralDirectory->getArchiveComment(); - } - - /** - * Set password to all input encrypted entries. - * - * @param string $password Password - * @return ZipFile - */ - public function withReadPassword($password) - { - foreach ($this->centralDirectory->getEntries() as $entry) { - if ($entry->isEncrypted()) { - $entry->setPassword($password); - } - } - return $this; + return $this->zipModel->getArchiveComment(); } /** * Set archive comment. * * @param null|string $comment + * @return ZipFileInterface * @throws InvalidArgumentException Length comment out of range */ public function setArchiveComment($comment = null) { - $this->centralDirectory->getEndOfCentralDirectory()->setComment($comment); + $this->zipModel->setArchiveComment($comment); + return $this; + } + + /** + * Checks that the entry in the archive is a directory. + * Returns true if and only if this ZIP entry represents a directory entry + * (i.e. end with '/'). + * + * @param string $entryName + * @return bool + * @throws ZipNotFoundEntry + */ + public function isDirectory($entryName) + { + return $this->zipModel->getEntry($entryName)->isDirectory(); } /** @@ -246,7 +220,7 @@ class ZipFile implements \Countable, \ArrayAccess, \Iterator */ public function getEntryComment($entryName) { - return $this->centralDirectory->getEntry($entryName)->getComment(); + return $this->zipModel->getEntry($entryName)->getComment(); } /** @@ -254,15 +228,37 @@ class ZipFile implements \Countable, \ArrayAccess, \Iterator * * @param string $entryName * @param string|null $comment - * @return ZipFile + * @return ZipFileInterface * @throws ZipNotFoundEntry */ public function setEntryComment($entryName, $comment = null) { - $this->centralDirectory->setEntryComment($entryName, $comment); + $this->zipModel->getEntryForChanges($entryName)->setComment($comment); return $this; } + /** + * Returns the entry contents. + * + * @param string $entryName + * @return string + */ + public function getEntryContents($entryName) + { + return $this->zipModel->getEntry($entryName)->getEntryContent(); + } + + /** + * Checks if there is an entry in the archive. + * + * @param string $entryName + * @return bool + */ + public function hasEntry($entryName) + { + return $this->zipModel->hasEntry($entryName); + } + /** * Get info by entry. * @@ -272,10 +268,7 @@ class ZipFile implements \Countable, \ArrayAccess, \Iterator */ public function getEntryInfo($entryName) { - if (!($entryName instanceof ZipEntry)) { - $entryName = $this->centralDirectory->getEntry($entryName); - } - return new ZipInfo($entryName); + return new ZipInfo($this->zipModel->getEntry($entryName)); } /** @@ -285,7 +278,15 @@ class ZipFile implements \Countable, \ArrayAccess, \Iterator */ public function getAllInfo() { - return array_map([$this, 'getEntryInfo'], $this->centralDirectory->getEntries()); + return array_map([$this, 'getEntryInfo'], $this->zipModel->getEntries()); + } + + /** + * @return ZipEntryMatcher + */ + public function matcher() + { + return $this->zipModel->matcher(); } /** @@ -296,7 +297,7 @@ class ZipFile implements \Countable, \ArrayAccess, \Iterator * @param string $destination 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. - * @return ZipFile + * @return ZipFileInterface * @throws ZipException */ public function extractTo($destination, $entries = null) @@ -311,9 +312,8 @@ class ZipFile implements \Countable, \ArrayAccess, \Iterator throw new ZipException("Destination is not writable directory"); } - /** - * @var ZipEntry[] $zipEntries - */ + $zipEntries = $this->zipModel->getEntries(); + if (!empty($entries)) { if (is_string($entries)) { $entries = (array)$entries; @@ -321,18 +321,10 @@ class ZipFile implements \Countable, \ArrayAccess, \Iterator if (is_array($entries)) { $entries = array_unique($entries); $flipEntries = array_flip($entries); - $zipEntries = array_filter( - $this->centralDirectory->getEntries(), - function ($zipEntry) use ($flipEntries) { - /** - * @var ZipEntry $zipEntry - */ - return isset($flipEntries[$zipEntry->getName()]); - } - ); + $zipEntries = array_filter($zipEntries, function (ZipEntry $zipEntry) use ($flipEntries) { + return isset($flipEntries[$zipEntry->getName()]); + }); } - } else { - $zipEntries = $this->centralDirectory->getEntries(); } foreach ($zipEntries as $entry) { @@ -355,7 +347,7 @@ class ZipFile implements \Countable, \ArrayAccess, \Iterator chmod($dir, 0755); touch($dir, $entry->getTime()); } - if (file_put_contents($file, $entry->getEntryContent()) === false) { + if (false === file_put_contents($file, $entry->getEntryContent())) { throw new ZipException('Can not extract file ' . $entry->getName()); } touch($file, $entry->getTime()); @@ -371,12 +363,12 @@ class ZipFile implements \Countable, \ArrayAccess, \Iterator * @param int|null $compressionMethod Compression method. * Use ZipFile::METHOD_STORED, ZipFile::METHOD_DEFLATED or ZipFile::METHOD_BZIP2. * If null, then auto choosing method. - * @return ZipFile + * @return ZipFileInterface * @throws InvalidArgumentException If incorrect data or entry name. * @throws ZipUnsupportMethod - * @see ZipFile::METHOD_STORED - * @see ZipFile::METHOD_DEFLATED - * @see ZipFile::METHOD_BZIP2 + * @see ZipFileInterface::METHOD_STORED + * @see ZipFileInterface::METHOD_DEFLATED + * @see ZipFileInterface::METHOD_BZIP2 */ public function addFromString($localName, $contents, $compressionMethod = null) { @@ -390,23 +382,23 @@ class ZipFile implements \Countable, \ArrayAccess, \Iterator $contents = (string)$contents; $length = strlen($contents); if (null === $compressionMethod) { - if ($length >= 1024) { - $compressionMethod = self::METHOD_DEFLATED; + if ($length >= 512) { + $compressionMethod = ZipEntry::UNKNOWN; } else { - $compressionMethod = self::METHOD_STORED; + $compressionMethod = ZipFileInterface::METHOD_STORED; } } elseif (!in_array($compressionMethod, self::$allowCompressionMethods, true)) { - throw new ZipUnsupportMethod('Unsupported method ' . $compressionMethod); + throw new ZipUnsupportMethod('Unsupported compression method ' . $compressionMethod); } $externalAttributes = 0100644 << 16; - $entry = new ZipNewStringEntry($contents); + $entry = new ZipNewEntry($contents); $entry->setName($localName); $entry->setMethod($compressionMethod); $entry->setTime(time()); $entry->setExternalAttributes($externalAttributes); - $this->centralDirectory->putInModified($localName, $entry); + $this->zipModel->addEntry($entry); return $this; } @@ -418,12 +410,12 @@ class ZipFile implements \Countable, \ArrayAccess, \Iterator * @param int|null $compressionMethod Compression method. * Use ZipFile::METHOD_STORED, ZipFile::METHOD_DEFLATED or ZipFile::METHOD_BZIP2. * If null, then auto choosing method. - * @return ZipFile + * @return ZipFileInterface * @throws InvalidArgumentException * @throws ZipUnsupportMethod - * @see ZipFile::METHOD_STORED - * @see ZipFile::METHOD_DEFLATED - * @see ZipFile::METHOD_BZIP2 + * @see ZipFileInterface::METHOD_STORED + * @see ZipFileInterface::METHOD_DEFLATED + * @see ZipFileInterface::METHOD_BZIP2 */ public function addFile($filename, $localName = null, $compressionMethod = null) { @@ -439,16 +431,16 @@ class ZipFile implements \Countable, \ArrayAccess, \Iterator $mimeType = @mime_content_type($filename); $type = strtok($mimeType, '/'); if ('image' === $type) { - $compressionMethod = self::METHOD_STORED; + $compressionMethod = ZipFileInterface::METHOD_STORED; } elseif ('text' === $type && filesize($filename) < 150) { - $compressionMethod = self::METHOD_STORED; + $compressionMethod = ZipFileInterface::METHOD_STORED; } else { - $compressionMethod = self::METHOD_DEFLATED; + $compressionMethod = ZipEntry::UNKNOWN; } - } elseif (@filesize($filename) >= 1024) { - $compressionMethod = self::METHOD_DEFLATED; + } elseif (@filesize($filename) >= 512) { + $compressionMethod = ZipEntry::UNKNOWN; } else { - $compressionMethod = self::METHOD_STORED; + $compressionMethod = ZipFileInterface::METHOD_STORED; } } elseif (!in_array($compressionMethod, self::$allowCompressionMethods, true)) { throw new ZipUnsupportMethod('Unsupported method ' . $compressionMethod); @@ -461,9 +453,7 @@ class ZipFile implements \Countable, \ArrayAccess, \Iterator $localName = basename($filename); } $this->addFromStream($handle, $localName, $compressionMethod); - $this->centralDirectory - ->getModifiedEntry($localName) - ->setTime(filemtime($filename)); + $this->zipModel->getEntry($localName)->setTime(filemtime($filename)); return $this; } @@ -475,12 +465,12 @@ class ZipFile implements \Countable, \ArrayAccess, \Iterator * @param int|null $compressionMethod Compression method. * Use ZipFile::METHOD_STORED, ZipFile::METHOD_DEFLATED or ZipFile::METHOD_BZIP2. * If null, then auto choosing method. - * @return ZipFile + * @return ZipFileInterface * @throws InvalidArgumentException * @throws ZipUnsupportMethod - * @see ZipFile::METHOD_STORED - * @see ZipFile::METHOD_DEFLATED - * @see ZipFile::METHOD_BZIP2 + * @see ZipFileInterface::METHOD_STORED + * @see ZipFileInterface::METHOD_DEFLATED + * @see ZipFileInterface::METHOD_BZIP2 */ public function addFromStream($stream, $localName, $compressionMethod = null) { @@ -494,10 +484,10 @@ class ZipFile implements \Countable, \ArrayAccess, \Iterator $fstat = fstat($stream); $length = $fstat['size']; if (null === $compressionMethod) { - if ($length >= 1024) { - $compressionMethod = self::METHOD_DEFLATED; + if ($length >= 512) { + $compressionMethod = ZipEntry::UNKNOWN; } else { - $compressionMethod = self::METHOD_STORED; + $compressionMethod = ZipFileInterface::METHOD_STORED; } } elseif (!in_array($compressionMethod, self::$allowCompressionMethods, true)) { throw new ZipUnsupportMethod('Unsupported method ' . $compressionMethod); @@ -506,13 +496,13 @@ class ZipFile implements \Countable, \ArrayAccess, \Iterator $mode = sprintf('%o', $fstat['mode']); $externalAttributes = (octdec($mode) & 0xffff) << 16; - $entry = new ZipNewStreamEntry($stream); + $entry = new ZipNewEntry($stream); $entry->setName($localName); $entry->setMethod($compressionMethod); $entry->setTime(time()); $entry->setExternalAttributes($externalAttributes); - $this->centralDirectory->putInModified($localName, $entry); + $this->zipModel->addEntry($entry); return $this; } @@ -520,7 +510,7 @@ class ZipFile implements \Countable, \ArrayAccess, \Iterator * Add an empty directory in the zip archive. * * @param string $dirName - * @return ZipFile + * @return ZipFileInterface * @throws InvalidArgumentException */ public function addEmptyDir($dirName) @@ -532,33 +522,19 @@ class ZipFile implements \Countable, \ArrayAccess, \Iterator $dirName = rtrim($dirName, '/') . '/'; $externalAttributes = 040755 << 16; - $entry = new ZipNewEmptyDirEntry(); + $entry = new ZipNewEntry(); $entry->setName($dirName); $entry->setTime(time()); - $entry->setMethod(self::METHOD_STORED); + $entry->setMethod(ZipFileInterface::METHOD_STORED); $entry->setSize(0); $entry->setCompressedSize(0); $entry->setCrc(0); $entry->setExternalAttributes($externalAttributes); - $this->centralDirectory->putInModified($dirName, $entry); + $this->zipModel->addEntry($entry); return $this; } - /** - * Add array data to archive. - * Keys is local names. - * Values is contents. - * - * @param array $mapData Associative array for added to zip. - */ - public function addAll(array $mapData) - { - foreach ($mapData as $localName => $content) { - $this[$localName] = $content; - } - } - /** * Add directory not recursively to the zip archive. * @@ -567,7 +543,7 @@ class ZipFile implements \Countable, \ArrayAccess, \Iterator * @param int|null $compressionMethod Compression method. * Use ZipFile::METHOD_STORED, ZipFile::METHOD_DEFLATED or ZipFile::METHOD_BZIP2. * If null, then auto choosing method. - * @return ZipFile + * @return ZipFileInterface * @throws InvalidArgumentException */ public function addDir($inputDir, $localPath = "/", $compressionMethod = null) @@ -593,12 +569,12 @@ class ZipFile implements \Countable, \ArrayAccess, \Iterator * @param int|null $compressionMethod Compression method. * Use ZipFile::METHOD_STORED, ZipFile::METHOD_DEFLATED or ZipFile::METHOD_BZIP2. * If null, then auto choosing method. - * @return ZipFile + * @return ZipFileInterface * @throws InvalidArgumentException * @throws ZipUnsupportMethod - * @see ZipFile::METHOD_STORED - * @see ZipFile::METHOD_DEFLATED - * @see ZipFile::METHOD_BZIP2 + * @see ZipFileInterface::METHOD_STORED + * @see ZipFileInterface::METHOD_DEFLATED + * @see ZipFileInterface::METHOD_BZIP2 */ public function addDirRecursive($inputDir, $localPath = "/", $compressionMethod = null) { @@ -623,19 +599,18 @@ class ZipFile implements \Countable, \ArrayAccess, \Iterator * @param int|null $compressionMethod Compression method. * Use ZipFile::METHOD_STORED, ZipFile::METHOD_DEFLATED or ZipFile::METHOD_BZIP2. * If null, then auto choosing method. - * @return ZipFile + * @return ZipFileInterface * @throws InvalidArgumentException * @throws ZipUnsupportMethod - * @see ZipFile::METHOD_STORED - * @see ZipFile::METHOD_DEFLATED - * @see ZipFile::METHOD_BZIP2 + * @see ZipFileInterface::METHOD_STORED + * @see ZipFileInterface::METHOD_DEFLATED + * @see ZipFileInterface::METHOD_BZIP2 */ public function addFilesFromIterator( \Iterator $iterator, $localPath = '/', $compressionMethod = null - ) - { + ) { $localPath = (string)$localPath; if (null !== $localPath && 0 !== strlen($localPath)) { $localPath = rtrim($localPath, '/'); @@ -690,7 +665,7 @@ class ZipFile implements \Countable, \ArrayAccess, \Iterator * @param int|null $compressionMethod Compression method. * Use ZipFile::METHOD_STORED, ZipFile::METHOD_DEFLATED or ZipFile::METHOD_BZIP2. * If null, then auto choosing method. - * @return ZipFile + * @return ZipFileInterface * @throws InvalidArgumentException * @sse https://en.wikipedia.org/wiki/Glob_(programming) Glob pattern syntax */ @@ -699,24 +674,6 @@ class ZipFile implements \Countable, \ArrayAccess, \Iterator return $this->addGlob($inputDir, $globPattern, $localPath, false, $compressionMethod); } - /** - * Add files recursively from glob pattern. - * - * @param string $inputDir Input directory - * @param string $globPattern Glob pattern. - * @param string|null $localPath Add files to this directory, or the root. - * @param int|null $compressionMethod Compression method. - * Use ZipFile::METHOD_STORED, ZipFile::METHOD_DEFLATED or ZipFile::METHOD_BZIP2. - * If null, then auto choosing method. - * @return ZipFile - * @throws InvalidArgumentException - * @sse https://en.wikipedia.org/wiki/Glob_(programming) Glob pattern syntax - */ - public function addFilesFromGlobRecursive($inputDir, $globPattern, $localPath = '/', $compressionMethod = null) - { - return $this->addGlob($inputDir, $globPattern, $localPath, true, $compressionMethod); - } - /** * Add files from glob pattern. * @@ -727,7 +684,7 @@ class ZipFile implements \Countable, \ArrayAccess, \Iterator * @param int|null $compressionMethod Compression method. * Use ZipFile::METHOD_STORED, ZipFile::METHOD_DEFLATED or ZipFile::METHOD_BZIP2. * If null, then auto choosing method. - * @return ZipFile + * @return ZipFileInterface * @throws InvalidArgumentException * @sse https://en.wikipedia.org/wiki/Glob_(programming) Glob pattern syntax */ @@ -737,8 +694,7 @@ class ZipFile implements \Countable, \ArrayAccess, \Iterator $localPath = '/', $recursive = true, $compressionMethod = null - ) - { + ) { $inputDir = (string)$inputDir; if (null === $inputDir || 0 === strlen($inputDir)) { throw new InvalidArgumentException('Input dir empty'); @@ -779,6 +735,24 @@ class ZipFile implements \Countable, \ArrayAccess, \Iterator return $this; } + /** + * Add files recursively from glob pattern. + * + * @param string $inputDir Input directory + * @param string $globPattern Glob pattern. + * @param string|null $localPath Add files to this directory, or the root. + * @param int|null $compressionMethod Compression method. + * Use ZipFile::METHOD_STORED, ZipFile::METHOD_DEFLATED or ZipFile::METHOD_BZIP2. + * If null, then auto choosing method. + * @return ZipFileInterface + * @throws InvalidArgumentException + * @sse https://en.wikipedia.org/wiki/Glob_(programming) Glob pattern syntax + */ + public function addFilesFromGlobRecursive($inputDir, $globPattern, $localPath = '/', $compressionMethod = null) + { + return $this->addGlob($inputDir, $globPattern, $localPath, true, $compressionMethod); + } + /** * Add files from regex pattern. * @@ -788,7 +762,7 @@ class ZipFile implements \Countable, \ArrayAccess, \Iterator * @param int|null $compressionMethod Compression method. * Use ZipFile::METHOD_STORED, ZipFile::METHOD_DEFLATED or ZipFile::METHOD_BZIP2. * If null, then auto choosing method. - * @return ZipFile + * @return ZipFileInterface * @internal param bool $recursive Recursive search. */ public function addFilesFromRegex($inputDir, $regexPattern, $localPath = "/", $compressionMethod = null) @@ -796,24 +770,6 @@ class ZipFile implements \Countable, \ArrayAccess, \Iterator return $this->addRegex($inputDir, $regexPattern, $localPath, false, $compressionMethod); } - /** - * Add files recursively from regex pattern. - * - * @param string $inputDir Search files in this directory. - * @param string $regexPattern Regex pattern. - * @param string|null $localPath Add files to this directory, or the root. - * @param int|null $compressionMethod Compression method. - * Use ZipFile::METHOD_STORED, ZipFile::METHOD_DEFLATED or ZipFile::METHOD_BZIP2. - * If null, then auto choosing method. - * @return ZipFile - * @internal param bool $recursive Recursive search. - */ - public function addFilesFromRegexRecursive($inputDir, $regexPattern, $localPath = "/", $compressionMethod = null) - { - return $this->addRegex($inputDir, $regexPattern, $localPath, true, $compressionMethod); - } - - /** * Add files from regex pattern. * @@ -824,7 +780,7 @@ class ZipFile implements \Countable, \ArrayAccess, \Iterator * @param int|null $compressionMethod Compression method. * Use ZipFile::METHOD_STORED, ZipFile::METHOD_DEFLATED or ZipFile::METHOD_BZIP2. * If null, then auto choosing method. - * @return ZipFile + * @return ZipFileInterface * @throws InvalidArgumentException */ private function addRegex( @@ -833,8 +789,7 @@ class ZipFile implements \Countable, \ArrayAccess, \Iterator $localPath = "/", $recursive = true, $compressionMethod = null - ) - { + ) { $regexPattern = (string)$regexPattern; if (empty($regexPattern)) { throw new InvalidArgumentException("regex pattern empty"); @@ -874,12 +829,43 @@ class ZipFile implements \Countable, \ArrayAccess, \Iterator return $this; } + /** + * Add files recursively from regex pattern. + * + * @param string $inputDir Search files in this directory. + * @param string $regexPattern Regex pattern. + * @param string|null $localPath Add files to this directory, or the root. + * @param int|null $compressionMethod Compression method. + * Use ZipFile::METHOD_STORED, ZipFile::METHOD_DEFLATED or ZipFile::METHOD_BZIP2. + * If null, then auto choosing method. + * @return ZipFileInterface + * @internal param bool $recursive Recursive search. + */ + public function addFilesFromRegexRecursive($inputDir, $regexPattern, $localPath = "/", $compressionMethod = null) + { + return $this->addRegex($inputDir, $regexPattern, $localPath, true, $compressionMethod); + } + + /** + * Add array data to archive. + * Keys is local names. + * Values is contents. + * + * @param array $mapData Associative array for added to zip. + */ + public function addAll(array $mapData) + { + foreach ($mapData as $localName => $content) { + $this[$localName] = $content; + } + } + /** * Rename the entry. * * @param string $oldName Old entry name. * @param string $newName New entry name. - * @return ZipFile + * @return ZipFileInterface * @throws InvalidArgumentException * @throws ZipNotFoundEntry */ @@ -888,7 +874,9 @@ class ZipFile implements \Countable, \ArrayAccess, \Iterator if (null === $oldName || null === $newName) { throw new InvalidArgumentException("name is null"); } - $this->centralDirectory->rename($oldName, $newName); + if ($oldName !== $newName) { + $this->zipModel->renameEntry($oldName, $newName); + } return $this; } @@ -896,13 +884,15 @@ class ZipFile implements \Countable, \ArrayAccess, \Iterator * Delete entry by name. * * @param string $entryName Zip Entry name. - * @return ZipFile + * @return ZipFileInterface * @throws ZipNotFoundEntry If entry not found. */ public function deleteFromName($entryName) { $entryName = (string)$entryName; - $this->centralDirectory->deleteEntry($entryName); + if (!$this->zipModel->deleteEntry($entryName)) { + throw new ZipNotFoundEntry("Entry " . $entryName . ' not found!'); + } return $this; } @@ -910,7 +900,7 @@ class ZipFile implements \Countable, \ArrayAccess, \Iterator * Delete entries by glob pattern. * * @param string $globPattern Glob pattern - * @return ZipFile + * @return ZipFileInterface * @throws InvalidArgumentException * @sse https://en.wikipedia.org/wiki/Glob_(programming) Glob pattern syntax */ @@ -928,7 +918,7 @@ class ZipFile implements \Countable, \ArrayAccess, \Iterator * Delete entries by regex pattern. * * @param string $regexPattern Regex pattern - * @return ZipFile + * @return ZipFileInterface * @throws InvalidArgumentException */ public function deleteFromRegex($regexPattern) @@ -936,17 +926,17 @@ class ZipFile implements \Countable, \ArrayAccess, \Iterator if (null === $regexPattern || !is_string($regexPattern) || empty($regexPattern)) { throw new InvalidArgumentException("Regex pattern is empty."); } - $this->centralDirectory->deleteEntriesFromRegex($regexPattern); + $this->matcher()->match($regexPattern)->delete(); return $this; } /** * Delete all entries - * @return ZipFile + * @return ZipFileInterface */ public function deleteAll() { - $this->centralDirectory->deleteAll(); + $this->zipModel->deleteAll(); return $this; } @@ -954,43 +944,239 @@ class ZipFile implements \Countable, \ArrayAccess, \Iterator * Set compression level for new entries. * * @param int $compressionLevel - * @see ZipFile::LEVEL_DEFAULT_COMPRESSION - * @see ZipFile::LEVEL_BEST_SPEED - * @see ZipFile::LEVEL_BEST_COMPRESSION + * @return ZipFileInterface + * @throws InvalidArgumentException + * @see ZipFileInterface::LEVEL_DEFAULT_COMPRESSION + * @see ZipFileInterface::LEVEL_SUPER_FAST + * @see ZipFileInterface::LEVEL_FAST + * @see ZipFileInterface::LEVEL_BEST_COMPRESSION */ - public function setCompressionLevel($compressionLevel = self::LEVEL_DEFAULT_COMPRESSION) + public function setCompressionLevel($compressionLevel = ZipFileInterface::LEVEL_DEFAULT_COMPRESSION) { - $this->centralDirectory->setCompressionLevel($compressionLevel); + if ($compressionLevel < ZipFileInterface::LEVEL_DEFAULT_COMPRESSION || + $compressionLevel > ZipFileInterface::LEVEL_BEST_COMPRESSION + ) { + throw new InvalidArgumentException('Invalid compression level. Minimum level ' . + ZipFileInterface::LEVEL_DEFAULT_COMPRESSION . '. Maximum level ' . ZipFileInterface::LEVEL_BEST_COMPRESSION); + } + $this->matcher()->all()->invoke(function ($entry) use ($compressionLevel) { + $this->setCompressionLevelEntry($entry, $compressionLevel); + }); + return $this; } /** + * @param string $entryName + * @param int $compressionLevel + * @return ZipFileInterface + * @throws ZipException + * @see ZipFileInterface::LEVEL_DEFAULT_COMPRESSION + * @see ZipFileInterface::LEVEL_SUPER_FAST + * @see ZipFileInterface::LEVEL_FAST + * @see ZipFileInterface::LEVEL_BEST_COMPRESSION + */ + public function setCompressionLevelEntry($entryName, $compressionLevel) + { + if (null !== $compressionLevel) { + if ($compressionLevel < ZipFileInterface::LEVEL_DEFAULT_COMPRESSION || + $compressionLevel > ZipFileInterface::LEVEL_BEST_COMPRESSION + ) { + throw new InvalidArgumentException('Invalid compression level. Minimum level ' . + ZipFileInterface::LEVEL_DEFAULT_COMPRESSION . '. Maximum level ' . ZipFileInterface::LEVEL_BEST_COMPRESSION); + } + $entry = $this->zipModel->getEntry($entryName); + if ($entry->getCompressionLevel() !== $compressionLevel) { + $entry = $this->zipModel->getEntryForChanges($entry); + $entry->setCompressionLevel($compressionLevel); + } + } + return $this; + } + + /** + * @param string $entryName + * @param int $compressionMethod + * @return ZipFileInterface + * @throws ZipException + * @see ZipFileInterface::METHOD_STORED + * @see ZipFileInterface::METHOD_DEFLATED + * @see ZipFileInterface::METHOD_BZIP2 + */ + public function setCompressionMethodEntry($entryName, $compressionMethod) + { + if (!in_array($compressionMethod, self::$allowCompressionMethods, true)) { + throw new ZipUnsupportMethod('Unsupported method ' . $compressionMethod); + } + $entry = $this->zipModel->getEntry($entryName); + if ($entry->getMethod() !== $compressionMethod) { + $this->zipModel + ->getEntryForChanges($entry) + ->setMethod($compressionMethod); + } + return $this; + } + + /** + * zipalign is optimization to Android application (APK) files. + * * @param int|null $align + * @return ZipFileInterface + * @link https://developer.android.com/studio/command-line/zipalign.html */ public function setZipAlign($align = null) { - $this->centralDirectory->setZipAlign($align); + $this->zipModel->setZipAlign($align); + return $this; + } + + /** + * Set password to all input encrypted entries. + * + * @param string $password Password + * @return ZipFileInterface + * @deprecated using ZipFileInterface::setReadPassword() + */ + public function withReadPassword($password) + { + return $this->setReadPassword($password); + } + + /** + * Set password to all input encrypted entries. + * + * @param string $password Password + * @return ZipFileInterface + */ + public function setReadPassword($password) + { + $this->zipModel->setReadPassword($password); + return $this; + } + + /** + * Set password to concrete input entry. + * + * @param string $entryName + * @param string $password Password + * @return ZipFileInterface + */ + public function setReadPasswordEntry($entryName, $password) + { + $this->zipModel->setReadPasswordEntry($entryName, $password); + return $this; } /** * Set password for all entries for update. * * @param string $password If password null then encryption clear - * @param int $encryptionMethod Encryption method - * @return ZipFile + * @param int|null $encryptionMethod Encryption method + * @return ZipFileInterface + * @deprecated using ZipFileInterface::setPassword() */ - public function withNewPassword($password, $encryptionMethod = self::ENCRYPTION_METHOD_WINZIP_AES) + public function withNewPassword($password, $encryptionMethod = self::ENCRYPTION_METHOD_WINZIP_AES_256) { - $this->centralDirectory->setNewPassword($password, $encryptionMethod); + return $this->setPassword($password, $encryptionMethod); + } + + /** + * Set password for zip archive + * + * @param string $password + * @param int|null $encryptionMethod Encryption method + * @return ZipFileInterface + * @throws ZipException + */ + public function setPassword($password, $encryptionMethod = self::ENCRYPTION_METHOD_WINZIP_AES_256) + { + $this->zipModel->setWritePassword($password); + if (null !== $encryptionMethod) { + if (!in_array($encryptionMethod, self::$allowEncryptionMethods)) { + throw new ZipException('Invalid encryption method'); + } + $this->zipModel->setEncryptionMethod($encryptionMethod); + } + return $this; + } + + /** + * @param string $entryName + * @param string|null $password + * @param int|null $encryptionMethod + * @return ZipFileInterface + * @throws ZipException + */ + public function setPasswordEntry($entryName, $password, $encryptionMethod = null) + { + if (null !== $encryptionMethod) { + if (!in_array($encryptionMethod, self::$allowEncryptionMethods)) { + throw new ZipException('Invalid encryption method'); + } + } + $this->matcher()->add($entryName)->setPassword($password, $encryptionMethod); return $this; } /** * Remove password for all entries for update. - * @return ZipFile + * @return ZipFileInterface + * @deprecated using ZipFileInterface::removePassword() */ public function withoutPassword() { - $this->centralDirectory->setNewPassword(null); + return $this->removePassword(); + } + + /** + * Remove password for all entries for update. + * @return ZipFileInterface + */ + public function removePassword() + { + $this->zipModel->removePassword(); + return $this; + } + + /** + * Remove password for concrete entry. + * @param string $entryName + * @return ZipFileInterface + */ + public function removePasswordEntry($entryName) + { + $this->zipModel->removePasswordEntry($entryName); + return $this; + } + + /** + * Undo all changes done in the archive + * @return ZipFileInterface + */ + public function unchangeAll() + { + $this->zipModel->unchangeAll(); + return $this; + } + + /** + * Undo change archive comment + * @return ZipFileInterface + */ + public function unchangeArchiveComment() + { + $this->zipModel->unchangeArchiveComment(); + return $this; + } + + /** + * Revert all changes done to an entry with the given name. + * + * @param string|ZipEntry $entry Entry name or ZipEntry + * @return ZipFileInterface + */ + public function unchangeEntry($entry) + { + $this->zipModel->unchangeEntry($entry); return $this; } @@ -998,6 +1184,7 @@ class ZipFile implements \Countable, \ArrayAccess, \Iterator * Save as file. * * @param string $filename Output filename + * @return ZipFileInterface * @throws InvalidArgumentException * @throws ZipException */ @@ -1014,12 +1201,14 @@ class ZipFile implements \Countable, \ArrayAccess, \Iterator if (!@rename($tempFilename, $filename)) { throw new ZipException('Can not move ' . $tempFilename . ' to ' . $filename); } + return $this; } /** * Save as stream. * * @param resource $handle Output stream resource + * @return ZipFileInterface * @throws ZipException */ public function saveAsStream($handle) @@ -1028,8 +1217,9 @@ class ZipFile implements \Countable, \ArrayAccess, \Iterator throw new InvalidArgumentException('handle is not resource'); } ftruncate($handle, 0); - $this->centralDirectory->writeArchive($handle); + $this->writeZipToStream($handle); fclose($handle); + return $this; } /** @@ -1061,11 +1251,60 @@ class ZipFile implements \Countable, \ArrayAccess, \Iterator $this->close(); header("Content-Type: " . $mimeType); - header("Content-Disposition: attachment; filename=" . rawurlencode($outputFilename)); + header('Content-Disposition: attachment; filename="' . $outputFilename . '"'); header("Content-Length: " . strlen($content)); exit($content); } + /** + * Output .ZIP archive as PSR-Message Response. + * + * @param ResponseInterface $response + * @param string $outputFilename + * @param string|null $mimeType + * @return ResponseInterface + * @throws InvalidArgumentException + */ + public function outputAsResponse(ResponseInterface $response, $outputFilename, $mimeType = null) + { + $outputFilename = (string)$outputFilename; + if (strlen($outputFilename) === 0) { + throw new InvalidArgumentException("Output filename is empty."); + } + if (empty($mimeType) || !is_string($mimeType)) { + $ext = strtolower(pathinfo($outputFilename, PATHINFO_EXTENSION)); + + if (!empty($ext) && isset(self::$defaultMimeTypes[$ext])) { + $mimeType = self::$defaultMimeTypes[$ext]; + } else { + $mimeType = self::$defaultMimeTypes['zip']; + } + } + $outputFilename = basename($outputFilename); + + if (!($handle = fopen('php://memory', 'w+b'))) { + throw new InvalidArgumentException("Memory can not open from write."); + } + $this->writeZipToStream($handle); + rewind($handle); + + $stream = new ResponseStream($handle); + $response->withHeader('Content-Type', $mimeType); + $response->withHeader('Content-Disposition', 'attachment; filename="' . $outputFilename . '"'); + $response->withHeader('Content-Length', $stream->getSize()); + $response->withBody($stream); + return $response; + } + + /** + * @param $handle + */ + protected function writeZipToStream($handle) + { + $output = new ZipOutputStream($handle, $this->zipModel); + $output->writeZip(); + } + /** * Returns the zip archive as a string. * @return string @@ -1076,7 +1315,7 @@ class ZipFile implements \Countable, \ArrayAccess, \Iterator if (!($handle = fopen('php://memory', 'w+b'))) { throw new InvalidArgumentException("Memory can not open from write."); } - $this->centralDirectory->writeArchive($handle); + $this->writeZipToStream($handle); rewind($handle); $content = stream_get_contents($handle); fclose($handle); @@ -1084,8 +1323,20 @@ class ZipFile implements \Countable, \ArrayAccess, \Iterator } /** - * Rewrite and reopen zip archive. - * @return ZipFile + * Close zip archive and release input stream. + */ + public function close() + { + if (null !== $this->inputStream) { + $this->inputStream->close(); + $this->inputStream = null; + $this->zipModel = new ZipModel(); + } + } + + /** + * Save and reopen zip archive. + * @return ZipFileInterface * @throws ZipException */ public function rewrite() @@ -1093,7 +1344,7 @@ class ZipFile implements \Countable, \ArrayAccess, \Iterator if (null === $this->inputStream) { throw new ZipException('input stream is null'); } - $meta = stream_get_meta_data($this->inputStream); + $meta = stream_get_meta_data($this->inputStream->getStream()); $content = $this->outputAsString(); $this->close(); if ('plainfile' === $meta['wrapper_type']) { @@ -1108,53 +1359,14 @@ class ZipFile implements \Countable, \ArrayAccess, \Iterator return $this->openFromString($content); } - /** - * Close zip archive and release input stream. - */ - public function close() - { - if (null !== $this->inputStream) { - fclose($this->inputStream); - $this->inputStream = null; - } - if (null !== $this->centralDirectory) { - $this->centralDirectory->release(); - $this->centralDirectory = null; - } - } - /** * Release all resources */ - function __destruct() + public function __destruct() { $this->close(); } - /** - * Whether a offset exists - * @link http://php.net/manual/en/arrayaccess.offsetexists.php - * @param string $entryName An offset to check for. - * @return boolean true on success or false on failure. - * The return value will be casted to boolean if non-boolean was returned. - */ - public function offsetExists($entryName) - { - return isset($this->centralDirectory->getEntries()[$entryName]); - } - - /** - * Offset to retrieve - * @link http://php.net/manual/en/arrayaccess.offsetget.php - * @param string $entryName The offset to retrieve. - * @return string|null - * @throws ZipNotFoundEntry - */ - public function offsetGet($entryName) - { - return $this->centralDirectory->getEntry($entryName)->getEntryContent(); - } - /** * Offset to set * @link http://php.net/manual/en/arrayaccess.offsetset.php @@ -1183,10 +1395,12 @@ class ZipFile implements \Countable, \ArrayAccess, \Iterator $this->addFile($contents->getPathname(), $entryName); return; } - $contents = (string)$contents; - if ('/' === $entryName[strlen($entryName) - 1]) { + if (StringUtil::endsWith($entryName, '/')) { $this->addEmptyDir($entryName); + } elseif (is_resource($contents)) { + $this->addFromStream($contents, $entryName); } else { + $contents = (string)$contents; $this->addFromString($entryName, $contents); } } @@ -1214,14 +1428,15 @@ class ZipFile implements \Countable, \ArrayAccess, \Iterator } /** - * Move forward to next element - * @link http://php.net/manual/en/iterator.next.php - * @return void Any returned value is ignored. - * @since 5.0.0 + * Offset to retrieve + * @link http://php.net/manual/en/arrayaccess.offsetget.php + * @param string $entryName The offset to retrieve. + * @return string|null + * @throws ZipNotFoundEntry */ - public function next() + public function offsetGet($entryName) { - next($this->centralDirectory->getEntries()); + return $this->getEntryContents($entryName); } /** @@ -1232,7 +1447,18 @@ class ZipFile implements \Countable, \ArrayAccess, \Iterator */ public function key() { - return key($this->centralDirectory->getEntries()); + return key($this->zipModel->getEntries()); + } + + /** + * Move forward to next element + * @link http://php.net/manual/en/iterator.next.php + * @return void Any returned value is ignored. + * @since 5.0.0 + */ + public function next() + { + next($this->zipModel->getEntries()); } /** @@ -1247,6 +1473,18 @@ class ZipFile implements \Countable, \ArrayAccess, \Iterator return $this->offsetExists($this->key()); } + /** + * Whether a offset exists + * @link http://php.net/manual/en/arrayaccess.offsetexists.php + * @param string $entryName An offset to check for. + * @return boolean true on success or false on failure. + * The return value will be casted to boolean if non-boolean was returned. + */ + public function offsetExists($entryName) + { + return $this->hasEntry($entryName); + } + /** * Rewind the Iterator to the first element * @link http://php.net/manual/en/iterator.rewind.php @@ -1255,6 +1493,6 @@ class ZipFile implements \Countable, \ArrayAccess, \Iterator */ public function rewind() { - reset($this->centralDirectory->getEntries()); + reset($this->zipModel->getEntries()); } -} \ No newline at end of file +} diff --git a/src/PhpZip/ZipFileInterface.php b/src/PhpZip/ZipFileInterface.php new file mode 100644 index 0000000..09ddd23 --- /dev/null +++ b/src/PhpZip/ZipFileInterface.php @@ -0,0 +1,630 @@ +openFile($filename); + foreach ($zipFile as $name => $contents) { + $info = $zipFile->getEntryInfo($name); + self::assertEquals(strlen($contents), $info->getSize()); + } + $zipFile->close(); + + self::assertCorrectZipArchive($filename); + } + + /** + * Bug #8009 (cannot add again same entry to an archive) + * @see https://github.com/php/php-src/blob/master/ext/zip/tests/bug8009.phpt + */ + public function testBug8009() + { + $filename = __DIR__ . '/php-zip-ext-test-resources/bug8009.zip'; + + $zipFile = new ZipFile(); + $zipFile->openFile($filename); + $zipFile->addFromString('2.txt', '=)'); + $zipFile->saveAsFile($this->outputFilename); + $zipFile->close(); + + self::assertCorrectZipArchive($this->outputFilename); + + $zipFile->openFile($this->outputFilename); + self::assertCount(2, $zipFile); + self::assertTrue(isset($zipFile['1.txt'])); + self::assertTrue(isset($zipFile['2.txt'])); + self::assertEquals($zipFile['2.txt'], $zipFile['1.txt']); + $zipFile->close(); + } + + /** + * Bug #40228 (extractTo does not create recursive empty path) + * @see https://github.com/php/php-src/blob/master/ext/zip/tests/bug40228.phpt + * @see https://github.com/php/php-src/blob/master/ext/zip/tests/bug40228-mb.phpt + * @dataProvider provideBug40228 + * @param string $filename + */ + public function testBug40228($filename) + { + self::assertTrue(mkdir($this->outputDirname, 0755, true)); + + $zipFile = new ZipFile(); + $zipFile->openFile($filename); + $zipFile->extractTo($this->outputDirname); + $zipFile->close(); + + self::assertTrue(is_dir($this->outputDirname . '/test/empty')); + } + + public function provideBug40228() + { + return [ + [__DIR__ . '/php-zip-ext-test-resources/bug40228.zip'], + [__DIR__ . '/php-zip-ext-test-resources/bug40228私はガラスを食べられます.zip'], + ]; + } + + /** + * Bug #49072 (feof never returns true for damaged file in zip) + * @see https://github.com/php/php-src/blob/master/ext/zip/tests/bug49072.phpt + * @expectedException \PhpZip\Exception\Crc32Exception + * @expectedExceptionMessage file1 (expected CRC32 value 0xc935c834, but is actually 0x76301511) + */ + public function testBug49072() + { + $filename = __DIR__ . '/php-zip-ext-test-resources/bug49072.zip'; + + $zipFile = new ZipFile(); + $zipFile->openFile($filename); + $zipFile->getEntryContents('file1'); + } + + /** + * Bug #70752 (Depacking with wrong password leaves 0 length files) + * @see https://github.com/php/php-src/blob/master/ext/zip/tests/bug70752.phpt + * @expectedException \PhpZip\Exception\ZipAuthenticationException + * @expectedExceptionMessage Bad password for entry bug70752.txt + */ + public function testBug70752() + { + $filename = __DIR__ . '/php-zip-ext-test-resources/bug70752.zip'; + + self::assertTrue(mkdir($this->outputDirname, 0755, true)); + + $zipFile = new ZipFile(); + try { + $zipFile->openFile($filename); + $zipFile->setReadPassword('bar'); + $zipFile->extractTo($this->outputDirname); + self::markTestIncomplete('failed test'); + } catch (ZipAuthenticationException $exception) { + self::assertFalse(file_exists($this->outputDirname . '/bug70752.txt')); + $zipFile->close(); + throw $exception; + } + } + + /** + * Bug #12414 ( extracting files from damaged archives) + * @see https://github.com/php/php-src/blob/master/ext/zip/tests/pecl12414.phpt + */ + public function testPecl12414() + { + $filename = __DIR__ . '/php-zip-ext-test-resources/pecl12414.zip'; + + $entryName = 'MYLOGOV2.GFX'; + + $zipFile = new ZipFile(); + $zipFile->openFile($filename); + + $info = $zipFile->getEntryInfo($entryName); + self::assertTrue($info->getSize() > 0); + + $contents = $zipFile[$entryName]; + self::assertEquals(strlen($contents), $info->getSize()); + + $zipFile->close(); + } +} diff --git a/tests/PhpZip/ZipFileAddDirTest.php b/tests/PhpZip/ZipFileAddDirTest.php index f4a4753..039c1c3 100644 --- a/tests/PhpZip/ZipFileAddDirTest.php +++ b/tests/PhpZip/ZipFileAddDirTest.php @@ -1,4 +1,5 @@ 'Hidden file', 'text file.txt' => 'Text file', 'Текстовый документ.txt' => 'Текстовый документ', @@ -52,7 +53,7 @@ class ZipFileAddDirTest extends ZipTestCase } } - protected static function assertFilesResult(ZipFile $zipFile, array $actualResultFiles = [], $localPath = '/') + protected static function assertFilesResult(ZipFileInterface $zipFile, array $actualResultFiles = [], $localPath = '/') { $localPath = rtrim($localPath, '/'); $localPath = empty($localPath) ? "" : $localPath . '/'; @@ -134,6 +135,29 @@ class ZipFileAddDirTest extends ZipTestCase $zipFile->close(); } + public function testAddFilesFromIteratorEmptyLocalPath() + { + $localPath = ''; + + $directoryIterator = new \DirectoryIterator($this->outputDirname); + + $zipFile = new ZipFile(); + $zipFile->addFilesFromIterator($directoryIterator, $localPath); + $zipFile->saveAsFile($this->outputFilename); + $zipFile->close(); + + self::assertCorrectZipArchive($this->outputFilename); + + $zipFile->openFile($this->outputFilename); + self::assertFilesResult($zipFile, [ + '.hidden', + 'text file.txt', + 'Текстовый документ.txt', + 'empty dir/', + ]); + $zipFile->close(); + } + public function testAddFilesFromRecursiveIterator() { $localPath = 'to/project'; @@ -182,7 +206,8 @@ class ZipFileAddDirTest extends ZipTestCase $zipFile->close(); } - public function testAddFilesFromIteratorWithIgnoreFiles(){ + public function testAddFilesFromIteratorWithIgnoreFiles() + { $localPath = 'to/project'; $ignoreFiles = [ 'Текстовый документ.txt', @@ -207,7 +232,8 @@ class ZipFileAddDirTest extends ZipTestCase $zipFile->close(); } - public function testAddFilesFromRecursiveIteratorWithIgnoreFiles(){ + public function testAddFilesFromRecursiveIteratorWithIgnoreFiles() + { $localPath = 'to/project'; $ignoreFiles = [ '.hidden', @@ -354,6 +380,4 @@ class ZipFileAddDirTest extends ZipTestCase self::assertFilesResult($zipFile, array_keys(self::$files), $localPath); $zipFile->close(); } - - -} \ No newline at end of file +} diff --git a/tests/PhpZip/ZipFileTest.php b/tests/PhpZip/ZipFileTest.php index a9810e9..aa3b5d7 100644 --- a/tests/PhpZip/ZipFileTest.php +++ b/tests/PhpZip/ZipFileTest.php @@ -1,10 +1,12 @@ openFromStream("stream resource"); } + /** + * @expectedException \PhpZip\Exception\InvalidArgumentException + * @expectedExceptionMessage Invalid resource type - gd. + */ + public function testOpenFromStreamInvalidResourceType2() + { + $zipFile = new ZipFile(); + if (!extension_loaded("gd")) { + $this->markTestSkipped('not extension gd'); + } + $zipFile->openFromStream(imagecreate(1, 1)); + } + + /** + * @expectedException \PhpZip\Exception\InvalidArgumentException + * @expectedExceptionMessage Invalid stream type - dir. + */ + public function testOpenFromStreamInvalidResourceType3() + { + $zipFile = new ZipFile(); + $zipFile->openFromStream(opendir(__DIR__)); + } + /** * @expectedException \PhpZip\Exception\InvalidArgumentException * @expectedExceptionMessage Resource cannot seekable stream. @@ -169,8 +195,8 @@ class ZipFileTest extends ZipTestCase $zipFile = new ZipFile(); $zipFile ->addFromString('file', 'content') - ->saveAsFile($this->outputFilename); - $zipFile->close(); + ->saveAsFile($this->outputFilename) + ->close(); $handle = fopen($this->outputFilename, 'rb'); $zipFile->openFromStream($handle); @@ -186,16 +212,18 @@ class ZipFileTest extends ZipTestCase public function testEmptyArchive() { $zipFile = new ZipFile(); - $zipFile->saveAsFile($this->outputFilename); - $zipFile->close(); + $zipFile + ->saveAsFile($this->outputFilename) + ->close(); self::assertCorrectEmptyZip($this->outputFilename); self::assertTrue(mkdir($this->outputDirname, 0755, true)); $zipFile->openFile($this->outputFilename); self::assertEquals($zipFile->count(), 0); - $zipFile->extractTo($this->outputDirname); - $zipFile->close(); + $zipFile + ->extractTo($this->outputDirname) + ->close(); self::assertTrue(FilesUtil::isEmptyDir($this->outputDirname)); } @@ -213,18 +241,23 @@ class ZipFileTest extends ZipTestCase $fileExpected = $this->outputDirname . DIRECTORY_SEPARATOR . 'file_expected.zip'; $zipFile = new ZipFile(); - $zipFile->addDirRecursive(__DIR__); - $zipFile->saveAsFile($fileActual); + $zipFile->addDirRecursive(__DIR__.'/../../src'); + $sourceCount = $zipFile->count(); + self::assertTrue($sourceCount > 0); + $zipFile + ->saveAsFile($fileActual) + ->close(); self::assertCorrectZipArchive($fileActual); - $zipFile->close(); - $zipFile->openFile($fileActual); - $zipFile->saveAsFile($fileExpected); + $zipFile + ->openFile($fileActual) + ->saveAsFile($fileExpected); self::assertCorrectZipArchive($fileExpected); $zipFileExpected = new ZipFile(); $zipFileExpected->openFile($fileExpected); + self::assertEquals($zipFile->count(), $sourceCount); self::assertEquals($zipFileExpected->count(), $zipFile->count()); self::assertEquals($zipFileExpected->getListFiles(), $zipFile->getListFiles()); @@ -242,13 +275,13 @@ class ZipFileTest extends ZipTestCase * @see ZipOutputFile::addFromString() * @see ZipOutputFile::addFromFile() * @see ZipOutputFile::addFromStream() - * @see ZipFile::getEntryContent() + * @see ZipFile::getEntryContents() */ public function testCreateArchiveAndAddFiles() { $outputFromString = file_get_contents(__FILE__); $outputFromString2 = file_get_contents(dirname(dirname(__DIR__)) . DIRECTORY_SEPARATOR . 'README.md'); - $outputFromFile = file_get_contents(dirname(dirname(__DIR__)) . DIRECTORY_SEPARATOR . 'bootstrap.xml'); + $outputFromFile = file_get_contents(dirname(dirname(__DIR__)) . DIRECTORY_SEPARATOR . 'phpunit.xml'); $outputFromStream = file_get_contents(dirname(dirname(__DIR__)) . DIRECTORY_SEPARATOR . 'composer.json'); $filenameFromString = basename(__FILE__); @@ -266,15 +299,18 @@ class ZipFileTest extends ZipTestCase fwrite($tempStream, $outputFromStream); $zipFile = new ZipFile; - $zipFile->addFromString($filenameFromString, $outputFromString); - $zipFile->addFile($tempFile, $filenameFromFile); - $zipFile->addFromStream($tempStream, $filenameFromStream); - $zipFile->addEmptyDir($emptyDirName); + $zipFile + ->addFromString($filenameFromString, $outputFromString) + ->addFile($tempFile, $filenameFromFile) + ->addFromStream($tempStream, $filenameFromStream) + ->addEmptyDir($emptyDirName); $zipFile[$filenameFromString2] = $outputFromString2; $zipFile[$emptyDirName2] = null; $zipFile[$emptyDirName3] = 'this content ignoring'; - $zipFile->saveAsFile($this->outputFilename); - $zipFile->close(); + self::assertEquals(count($zipFile), 7); + $zipFile + ->saveAsFile($this->outputFilename) + ->close(); unlink($tempFile); self::assertCorrectZipArchive($this->outputFilename); @@ -304,6 +340,18 @@ class ZipFileTest extends ZipTestCase $zipFile->close(); } + public function testEmptyContent() + { + $zipFile = new ZipFile(); + $zipFile['file'] = ''; + $zipFile->saveAsFile($this->outputFilename); + $zipFile->close(); + + $zipFile->openFile($this->outputFilename); + self::assertEquals($zipFile['file'], ''); + $zipFile->close(); + } + /** * Test compression method from image file. */ @@ -330,7 +378,7 @@ class ZipFileTest extends ZipTestCase $zipFile->openFile($this->outputFilename); $info = $zipFile->getEntryInfo($basename); - self::assertEquals($info->getMethod(), 'No compression'); + self::assertEquals($info->getMethodName(), 'No compression'); $zipFile->close(); } @@ -343,7 +391,7 @@ class ZipFileTest extends ZipTestCase $newName = 'tests/' . $oldName; $zipFile = new ZipFile(); - $zipFile->addDirRecursive(__DIR__); + $zipFile->addDir(__DIR__); $zipFile->saveAsFile($this->outputFilename); $zipFile->close(); @@ -405,7 +453,6 @@ class ZipFileTest extends ZipTestCase /** * @expectedException \PhpZip\Exception\ZipNotFoundEntry - * @expectedExceptionMessage Not found entry */ public function testRenameEntryNotFound() { @@ -465,7 +512,6 @@ class ZipFileTest extends ZipTestCase /** * @expectedException \PhpZip\Exception\ZipNotFoundEntry - * @expectedExceptionMessage Not found entry entry */ public function testDeleteFromNameNotFoundEntry() { @@ -481,22 +527,32 @@ class ZipFileTest extends ZipTestCase $inputDir = dirname(dirname(__DIR__)); $zipFile = new ZipFile(); - $zipFile->addFilesFromGlobRecursive($inputDir, '**.{php,xml,json}', '/'); + $zipFile->addFilesFromGlobRecursive($inputDir, '**.{xml,json,md}', '/'); + self::assertTrue(isset($zipFile['composer.json'])); + self::assertTrue(isset($zipFile['phpunit.xml'])); $zipFile->saveAsFile($this->outputFilename); $zipFile->close(); self::assertCorrectZipArchive($this->outputFilename); $zipFile->openFile($this->outputFilename); + self::assertTrue(isset($zipFile['composer.json'])); + self::assertTrue(isset($zipFile['phpunit.xml'])); $zipFile->deleteFromGlob('**.{xml,json}'); + self::assertFalse(isset($zipFile['composer.json'])); + self::assertFalse(isset($zipFile['phpunit.xml'])); $zipFile->saveAsFile($this->outputFilename); $zipFile->close(); self::assertCorrectZipArchive($this->outputFilename); $zipFile->openFile($this->outputFilename); - self::assertFalse(isset($zipFile['composer.json'])); - self::assertFalse(isset($zipFile['bootstrap.xml'])); + self::assertTrue($zipFile->count() > 0); + + foreach ($zipFile->getListFiles() as $name) { + self::assertStringEndsWith('.md', $name); + } + $zipFile->close(); } @@ -528,7 +584,7 @@ class ZipFileTest extends ZipTestCase $inputDir = dirname(dirname(__DIR__)); $zipFile = new ZipFile(); - $zipFile->addFilesFromRegexRecursive($inputDir, '~\.(xml|php|json)$~i', 'Path'); + $zipFile->addFilesFromRegexRecursive($inputDir, '~\.(xml|json)$~i', 'Path'); $zipFile->saveAsFile($this->outputFilename); $zipFile->close(); @@ -546,7 +602,7 @@ class ZipFileTest extends ZipTestCase $zipFile->openFile($this->outputFilename); self::assertFalse(isset($zipFile['Path/composer.json'])); self::assertFalse(isset($zipFile['Path/test.txt'])); - self::assertTrue(isset($zipFile['Path/bootstrap.xml'])); + self::assertTrue(isset($zipFile['Path/phpunit.xml'])); $zipFile->close(); } @@ -576,7 +632,8 @@ class ZipFileTest extends ZipTestCase public function testDeleteAll() { $zipFile = new ZipFile(); - $zipFile->addDirRecursive(__DIR__); + $zipFile->addDirRecursive(dirname(dirname(__DIR__)) .DIRECTORY_SEPARATOR. 'src'); + self::assertTrue($zipFile->count() > 0); $zipFile->saveAsFile($this->outputFilename); $zipFile->close(); @@ -732,8 +789,7 @@ class ZipFileTest extends ZipTestCase } /** - * @expectedException \PhpZip\Exception\ZipException - * @expectedExceptionMessage Not found entry + * @expectedException \PhpZip\Exception\ZipNotFoundEntry */ public function testSetEntryCommentNotFoundEntry() { @@ -749,19 +805,19 @@ class ZipFileTest extends ZipTestCase $entries = [ '1' => [ 'data' => CryptoUtil::randomBytes(255), - 'method' => ZipFile::METHOD_STORED, + 'method' => ZipFileInterface::METHOD_STORED, 'expected' => 'No compression', ], '2' => [ 'data' => CryptoUtil::randomBytes(255), - 'method' => ZipFile::METHOD_DEFLATED, + 'method' => ZipFileInterface::METHOD_DEFLATED, 'expected' => 'Deflate', ], ]; if (extension_loaded("bz2")) { $entries['3'] = [ 'data' => CryptoUtil::randomBytes(255), - 'method' => ZipFile::METHOD_BZIP2, + 'method' => ZipFileInterface::METHOD_BZIP2, 'expected' => 'Bzip2', ]; } @@ -776,12 +832,12 @@ class ZipFileTest extends ZipTestCase self::assertCorrectZipArchive($this->outputFilename); $zipFile->openFile($this->outputFilename); - $zipFile->setCompressionLevel(ZipFile::LEVEL_BEST_COMPRESSION); + $zipFile->setCompressionLevel(ZipFileInterface::LEVEL_BEST_COMPRESSION); $zipAllInfo = $zipFile->getAllInfo(); foreach ($zipAllInfo as $entryName => $info) { self::assertEquals($zipFile[$entryName], $entries[$entryName]['data']); - self::assertEquals($info->getMethod(), $entries[$entryName]['expected']); + self::assertEquals($info->getMethodName(), $entries[$entryName]['expected']); $entryInfo = $zipFile->getEntryInfo($entryName); self::assertEquals($entryInfo, $info); } @@ -959,105 +1015,6 @@ class ZipFileTest extends ZipTestCase $zipFile->extractTo($this->outputDirname); } - /** - * Test archive password. - */ - public function testSetPassword() - { - $password = base64_encode(CryptoUtil::randomBytes(100)); - $badPassword = "sdgt43r23wefe"; - - // create encryption password with ZipCrypto - $zipFile = new ZipFile(); - $zipFile->addDirRecursive(__DIR__); - $zipFile->withNewPassword($password, ZipFile::ENCRYPTION_METHOD_TRADITIONAL); - $zipFile->saveAsFile($this->outputFilename); - $zipFile->close(); - - self::assertCorrectZipArchive($this->outputFilename, $password); - - // check bad password for ZipCrypto - $zipFile->openFile($this->outputFilename); - $zipFile->withReadPassword($badPassword); - foreach ($zipFile->getListFiles() as $entryName) { - try { - $zipFile[$entryName]; - self::fail("Expected Exception has not been raised."); - } catch (ZipAuthenticationException $ae) { - self::assertNotNull($ae); - } - } - - // check correct password for ZipCrypto - $zipFile->withReadPassword($password); - foreach ($zipFile->getAllInfo() as $info) { - self::assertTrue($info->isEncrypted()); - self::assertContains('ZipCrypto', $info->getMethod()); - $decryptContent = $zipFile[$info->getPath()]; - self::assertNotEmpty($decryptContent); - self::assertContains('withNewPassword($password, ZipFile::ENCRYPTION_METHOD_WINZIP_AES); - $zipFile->saveAsFile($this->outputFilename); - $zipFile->close(); - - self::assertCorrectZipArchive($this->outputFilename, $password); - - // check from WinZip AES encryption - $zipFile->openFile($this->outputFilename); - // set bad password WinZip AES - $zipFile->withReadPassword($badPassword); - foreach ($zipFile->getListFiles() as $entryName) { - try { - $zipFile[$entryName]; - self::fail("Expected Exception has not been raised."); - } catch (ZipAuthenticationException $ae) { - self::assertNotNull($ae); - } - } - - // set correct password WinZip AES - $zipFile->withReadPassword($password); - foreach ($zipFile->getAllInfo() as $info) { - self::assertTrue($info->isEncrypted()); - self::assertContains('WinZip', $info->getMethod()); - $decryptContent = $zipFile[$info->getPath()]; - self::assertNotEmpty($decryptContent); - self::assertContains('addFromString('file1', ''); - $zipFile->withoutPassword(); - $zipFile->addFromString('file2', ''); - $zipFile->saveAsFile($this->outputFilename); - $zipFile->close(); - - self::assertCorrectZipArchive($this->outputFilename); - - // check remove password - $zipFile->openFile($this->outputFilename); - foreach ($zipFile->getAllInfo() as $info) { - self::assertFalse($info->isEncrypted()); - } - $zipFile->close(); - } - - /** - * @expectedException \PhpZip\Exception\ZipException - * @expectedExceptionMessage Invalid encryption method - */ - public function testSetEncryptionMethodInvalid() - { - $zipFile = new ZipFile(); - $encryptionMethod = 9999; - $zipFile->withNewPassword('pass', $encryptionMethod); - $zipFile['entry'] = 'content'; - $zipFile->outputAsString(); - } - /** * @expectedException \PhpZip\Exception\InvalidArgumentException * @expectedExceptionMessage entryName is null @@ -1100,7 +1057,7 @@ class ZipFileTest extends ZipTestCase /** * @expectedException \PhpZip\Exception\ZipUnsupportMethod - * @expectedExceptionMessage Unsupported method + * @expectedExceptionMessage Unsupported compression method */ public function testAddFromStringUnsupportedMethod() { @@ -1141,8 +1098,8 @@ class ZipFileTest extends ZipTestCase $zipFile->openFile($this->outputFilename); $infoStored = $zipFile->getEntryInfo(basename($fileStored)); $infoDeflated = $zipFile->getEntryInfo(basename($fileDeflated)); - self::assertEquals($infoStored->getMethod(), 'No compression'); - self::assertEquals($infoDeflated->getMethod(), 'Deflate'); + self::assertEquals($infoStored->getMethodName(), 'No compression'); + self::assertEquals($infoDeflated->getMethodName(), 'Deflate'); $zipFile->close(); } @@ -1153,6 +1110,7 @@ class ZipFileTest extends ZipTestCase public function testAddFromStreamInvalidResource() { $zipFile = new ZipFile(); + /** @noinspection PhpParamsInspection */ $zipFile->addFromStream("invalid resource", "name"); } @@ -1206,8 +1164,8 @@ class ZipFileTest extends ZipTestCase $zipFile->openFile($this->outputFilename); $infoStored = $zipFile->getEntryInfo(basename($fileStored)); $infoDeflated = $zipFile->getEntryInfo(basename($fileDeflated)); - self::assertEquals($infoStored->getMethod(), 'No compression'); - self::assertEquals($infoDeflated->getMethod(), 'Deflate'); + self::assertEquals($infoStored->getMethodName(), 'No compression'); + self::assertEquals($infoDeflated->getMethodName(), 'Deflate'); $zipFile->close(); } @@ -1521,6 +1479,7 @@ class ZipFileTest extends ZipTestCase public function testSaveAsStreamBadStream() { $zipFile = new ZipFile(); + /** @noinspection PhpParamsInspection */ $zipFile->saveAsStream("bad stream"); } @@ -1550,13 +1509,13 @@ class ZipFileTest extends ZipTestCase $files['file' . $i . '.txt'] = CryptoUtil::randomBytes(255); } - $methods = [ZipFile::METHOD_STORED, ZipFile::METHOD_DEFLATED]; + $methods = [ZipFileInterface::METHOD_STORED, ZipFileInterface::METHOD_DEFLATED]; if (extension_loaded("bz2")) { - $methods[] = ZipFile::METHOD_BZIP2; + $methods[] = ZipFileInterface::METHOD_BZIP2; } $zipFile = new ZipFile(); - $zipFile->setCompressionLevel(ZipFile::LEVEL_BEST_SPEED); + $zipFile->setCompressionLevel(ZipFileInterface::LEVEL_BEST_SPEED); foreach ($files as $entryName => $content) { $zipFile->addFromString($entryName, $content, $methods[array_rand($methods)]); } @@ -1627,18 +1586,43 @@ class ZipFileTest extends ZipTestCase public function testArrayAccessAddFile() { $entryName = 'path/to/file.dat'; + $entryNameStream = 'path/to/' . basename(__FILE__); $zipFile = new ZipFile(); $zipFile[$entryName] = new \SplFileInfo(__FILE__); + $zipFile[$entryNameStream] = fopen(__FILE__, 'r'); $zipFile->saveAsFile($this->outputFilename); $zipFile->close(); self::assertCorrectZipArchive($this->outputFilename); $zipFile->openFile($this->outputFilename); - self::assertEquals(sizeof($zipFile), 1); + self::assertEquals(sizeof($zipFile), 2); self::assertTrue(isset($zipFile[$entryName])); + self::assertTrue(isset($zipFile[$entryNameStream])); self::assertEquals($zipFile[$entryName], file_get_contents(__FILE__)); + self::assertEquals($zipFile[$entryNameStream], file_get_contents(__FILE__)); + $zipFile->close(); + } + + public function testUnknownCompressionMethod() + { + $zipFile = new ZipFile(); + + $zipFile->addFromString('file', 'content', ZipEntry::UNKNOWN); + $zipFile->addFromString('file2', base64_encode(CryptoUtil::randomBytes(512)), ZipEntry::UNKNOWN); + + self::assertEquals($zipFile->getEntryInfo('file')->getMethodName(), 'Unknown'); + self::assertEquals($zipFile->getEntryInfo('file2')->getMethodName(), 'Unknown'); + + $zipFile->saveAsFile($this->outputFilename); + $zipFile->close(); + + $zipFile->openFile($this->outputFilename); + + self::assertEquals($zipFile->getEntryInfo('file')->getMethodName(), 'No compression'); + self::assertEquals($zipFile->getEntryInfo('file2')->getMethodName(), 'Deflate'); + $zipFile->close(); } @@ -1700,8 +1684,10 @@ class ZipFileTest extends ZipTestCase $zipFile = new ZipFile(); $zipFile['file'] = 'content'; $zipFile['file2'] = 'content2'; - $zipFile->saveAsFile($this->outputFilename); - $zipFile->close(); + self::assertEquals(count($zipFile), 2); + $zipFile + ->saveAsFile($this->outputFilename) + ->close(); $md5file = md5_file($this->outputFilename); @@ -1710,7 +1696,7 @@ class ZipFileTest extends ZipTestCase self::assertTrue(isset($zipFile['file'])); self::assertTrue(isset($zipFile['file2'])); $zipFile['file3'] = 'content3'; - self::assertEquals(count($zipFile), 2); + self::assertEquals(count($zipFile), 3); $zipFile = $zipFile->rewrite(); self::assertEquals(count($zipFile), 3); self::assertTrue(isset($zipFile['file'])); @@ -1758,14 +1744,14 @@ class ZipFileTest extends ZipTestCase /** * Test zip alignment. */ - public function testZipAlign() + public function testZipAlignSourceZip() { $zipFile = new ZipFile(); for ($i = 0; $i < 100; $i++) { $zipFile->addFromString( 'entry' . $i . '.txt', CryptoUtil::randomBytes(mt_rand(100, 4096)), - ZipFile::METHOD_STORED + ZipFileInterface::METHOD_STORED ); } $zipFile->saveAsFile($this->outputFilename); @@ -1774,7 +1760,9 @@ class ZipFileTest extends ZipTestCase self::assertCorrectZipArchive($this->outputFilename); $result = self::doZipAlignVerify($this->outputFilename); - if ($result === null) return; // zip align not installed + if ($result === null) { + return; + } // zip align not installed // check not zip align self::assertFalse($result); @@ -1791,13 +1779,16 @@ class ZipFileTest extends ZipTestCase // check zip align self::assertTrue($result); + } + public function testZipAlignNewFiles() + { $zipFile = new ZipFile(); for ($i = 0; $i < 100; $i++) { $zipFile->addFromString( 'entry' . $i . '.txt', CryptoUtil::randomBytes(mt_rand(100, 4096)), - ZipFile::METHOD_STORED + ZipFileInterface::METHOD_STORED ); } $zipFile->setZipAlign(4); @@ -1807,10 +1798,306 @@ class ZipFileTest extends ZipTestCase self::assertCorrectZipArchive($this->outputFilename); $result = self::doZipAlignVerify($this->outputFilename); + if ($result === null) { + return; + } // zip align not installed // check not zip align self::assertTrue($result); } + public function testZipAlignFromModifiedZipArchive() + { + $zipFile = new ZipFile(); + for ($i = 0; $i < 100; $i++) { + $zipFile->addFromString( + 'entry' . $i . '.txt', + CryptoUtil::randomBytes(mt_rand(100, 4096)), + ZipFileInterface::METHOD_STORED + ); + } + $zipFile->saveAsFile($this->outputFilename); + $zipFile->close(); + + self::assertCorrectZipArchive($this->outputFilename); + + $result = self::doZipAlignVerify($this->outputFilename); + if ($result === null) { + return; + } // zip align not installed + + // check not zip align + self::assertFalse($result); + + $zipFile->openFile($this->outputFilename); + $zipFile->deleteFromRegex("~entry2[\d]+\.txt$~s"); + for ($i = 0; $i < 100; $i++) { + $isStored = (bool)mt_rand(0, 1); + + $zipFile->addFromString( + 'entry_new_' . ($isStored ? 'stored' : 'deflated') . '_' . $i . '.txt', + CryptoUtil::randomBytes(mt_rand(100, 4096)), + $isStored ? + ZipFileInterface::METHOD_STORED : + ZipFileInterface::METHOD_DEFLATED + ); + } + $zipFile->setZipAlign(4); + $zipFile->saveAsFile($this->outputFilename); + $zipFile->close(); + + self::assertCorrectZipArchive($this->outputFilename); + + $result = self::doZipAlignVerify($this->outputFilename, true); + self::assertNotNull($result); + + // check zip align + self::assertTrue($result); + } + + public function testFilename0() + { + $zipFile = new ZipFile(); + $zipFile[0] = 0; + self::assertTrue(isset($zipFile[0])); + self::assertTrue(isset($zipFile['0'])); + self::assertCount(1, $zipFile); + $zipFile + ->saveAsFile($this->outputFilename) + ->close(); + + self::assertCorrectZipArchive($this->outputFilename); + + $zipFile->openFile($this->outputFilename); + self::assertTrue(isset($zipFile[0])); + self::assertTrue(isset($zipFile['0'])); + self::assertEquals($zipFile['0'], '0'); + self::assertCount(1, $zipFile); + $zipFile->close(); + + self::assertTrue(unlink($this->outputFilename)); + + $zipFile = new ZipFile(); + $zipFile->addFromString(0, 0); + self::assertTrue(isset($zipFile[0])); + self::assertTrue(isset($zipFile['0'])); + self::assertCount(1, $zipFile); + $zipFile + ->saveAsFile($this->outputFilename) + ->close(); + + self::assertCorrectZipArchive($this->outputFilename); + } + + public function testPsrResponse() + { + $zipFile = new ZipFile(); + for ($i = 0; $i < 10; $i++) { + $zipFile[$i] = $i; + } + $filename = 'file.jar'; + $response = $this->getMock(ResponseInterface::class); + $response = $zipFile->outputAsResponse($response, $filename); + $this->assertInstanceOf(ResponseInterface::class, $response); + } + + /** + * @expectedException \PhpZip\Exception\InvalidArgumentException + * @expectedExceptionMessage Output filename is empty. + */ + public function testInvalidPsrResponse() + { + $zipFile = new ZipFile(); + $zipFile['file'] = 'content'; + $response = $this->getMock(ResponseInterface::class); + $zipFile->outputAsResponse($response, ''); + } + + public function testCompressionLevel() + { + $zipFile = new ZipFile(); + $zipFile + ->addFromString('file', 'content', ZipFileInterface::METHOD_DEFLATED) + ->setCompressionLevelEntry('file', ZipFileInterface::LEVEL_BEST_COMPRESSION) + ->addFromString('file2', 'content', ZipFileInterface::METHOD_DEFLATED) + ->setCompressionLevelEntry('file2', ZipFileInterface::LEVEL_FAST) + ->addFromString('file3', 'content', ZipFileInterface::METHOD_DEFLATED) + ->setCompressionLevelEntry('file3', ZipFileInterface::LEVEL_SUPER_FAST) + ->addFromString('file4', 'content', ZipFileInterface::METHOD_DEFLATED) + ->setCompressionLevelEntry('file4', ZipFileInterface::LEVEL_DEFAULT_COMPRESSION) + ->saveAsFile($this->outputFilename) + ->close(); + + self::assertCorrectZipArchive($this->outputFilename); + + $zipFile->openFile($this->outputFilename); + self::assertEquals($zipFile->getEntryInfo('file') + ->getCompressionLevel(), ZipFileInterface::LEVEL_BEST_COMPRESSION); + self::assertEquals($zipFile->getEntryInfo('file2') + ->getCompressionLevel(), ZipFileInterface::LEVEL_FAST); + self::assertEquals($zipFile->getEntryInfo('file3') + ->getCompressionLevel(), ZipFileInterface::LEVEL_SUPER_FAST); + self::assertEquals($zipFile->getEntryInfo('file4') + ->getCompressionLevel(), ZipFileInterface::LEVEL_DEFAULT_COMPRESSION); + $zipFile->close(); + } + + /** + * @expectedException \PhpZip\Exception\InvalidArgumentException + * @expectedExceptionMessage Invalid compression level + */ + public function testInvalidCompressionLevel() + { + $zipFile = new ZipFile(); + $zipFile->addFromString('file', 'content'); + $zipFile->setCompressionLevel(15); + } + + /** + * @expectedException \PhpZip\Exception\InvalidArgumentException + * @expectedExceptionMessage Invalid compression level + */ + public function testInvalidCompressionLevelEntry() + { + $zipFile = new ZipFile(); + $zipFile->addFromString('file', 'content'); + $zipFile->setCompressionLevelEntry('file', 15); + } + + public function testCompressionGlobal() + { + $zipFile = new ZipFile(); + for ($i = 0; $i < 10; $i++) { + $zipFile->addFromString('file' . $i, 'content', ZipFileInterface::METHOD_DEFLATED); + } + $zipFile + ->setCompressionLevel(ZipFileInterface::LEVEL_BEST_SPEED) + ->saveAsFile($this->outputFilename) + ->close(); + + self::assertCorrectZipArchive($this->outputFilename); + + $zipFile->openFile($this->outputFilename); + $infoList = $zipFile->getAllInfo(); + array_walk($infoList, function (ZipInfo $zipInfo) { + self::assertEquals($zipInfo->getCompressionLevel(), ZipFileInterface::LEVEL_BEST_SPEED); + }); + $zipFile->close(); + } + + public function testCompressionMethodEntry() + { + $zipFile = new ZipFile(); + $zipFile->addFromString('file', 'content', ZipFileInterface::METHOD_STORED); + $zipFile->saveAsFile($this->outputFilename); + $zipFile->close(); + + $zipFile->openFile($this->outputFilename); + self::assertEquals($zipFile->getEntryInfo('file')->getMethodName(), 'No compression'); + $zipFile->setCompressionMethodEntry('file', ZipFileInterface::METHOD_DEFLATED); + self::assertEquals($zipFile->getEntryInfo('file')->getMethodName(), 'Deflate'); + + $zipFile->rewrite(); + self::assertEquals($zipFile->getEntryInfo('file')->getMethodName(), 'Deflate'); + } + + /** + * @expectedException \PhpZip\Exception\ZipUnsupportMethod + * @expectedExceptionMessage Unsupported method + */ + public function testInvalidCompressionMethodEntry() + { + $zipFile = new ZipFile(); + $zipFile->addFromString('file', 'content', ZipFileInterface::METHOD_STORED); + $zipFile->setCompressionMethodEntry('file', 99); + } + + public function testUnchangeAll() + { + $zipFile = new ZipFile(); + for ($i = 0; $i < 10; $i++) { + $zipFile[$i] = $i; + } + $zipFile->setArchiveComment('comment'); + self::assertCount(10, $zipFile); + self::assertEquals($zipFile->getArchiveComment(), 'comment'); + $zipFile->saveAsFile($this->outputFilename); + + $zipFile->unchangeAll(); + self::assertCount(0, $zipFile); + self::assertEquals($zipFile->getArchiveComment(), null); + $zipFile->close(); + + $zipFile->openFile($this->outputFilename); + self::assertCount(10, $zipFile); + self::assertEquals($zipFile->getArchiveComment(), 'comment'); + + for ($i = 10; $i < 100; $i++) { + $zipFile[$i] = $i; + } + $zipFile->setArchiveComment('comment 2'); + self::assertCount(100, $zipFile); + self::assertEquals($zipFile->getArchiveComment(), 'comment 2'); + + $zipFile->unchangeAll(); + self::assertCount(10, $zipFile); + self::assertEquals($zipFile->getArchiveComment(), 'comment'); + $zipFile->close(); + } + + public function testUnchangeArchiveComment() + { + $zipFile = new ZipFile(); + for ($i = 0; $i < 10; $i++) { + $zipFile[$i] = $i; + } + $zipFile->setArchiveComment('comment'); + self::assertEquals($zipFile->getArchiveComment(), 'comment'); + $zipFile->saveAsFile($this->outputFilename); + + $zipFile->unchangeArchiveComment(); + self::assertEquals($zipFile->getArchiveComment(), null); + $zipFile->close(); + + $zipFile->openFile($this->outputFilename); + self::assertEquals($zipFile->getArchiveComment(), 'comment'); + $zipFile->setArchiveComment('comment 2'); + self::assertEquals($zipFile->getArchiveComment(), 'comment 2'); + + $zipFile->unchangeArchiveComment(); + self::assertEquals($zipFile->getArchiveComment(), 'comment'); + $zipFile->close(); + } + + public function testUnchangeEntry() + { + $zipFile = new ZipFile(); + $zipFile['file 1'] = 'content 1'; + $zipFile['file 2'] = 'content 2'; + $zipFile + ->saveAsFile($this->outputFilename) + ->close(); + + $zipFile->openFile($this->outputFilename); + + $zipFile['file 1'] = 'modify content 1'; + $zipFile->setPasswordEntry('file 1', 'password'); + + self::assertEquals($zipFile['file 1'], 'modify content 1'); + self::assertTrue($zipFile->getEntryInfo('file 1')->isEncrypted()); + + self::assertEquals($zipFile['file 2'], 'content 2'); + self::assertFalse($zipFile->getEntryInfo('file 2')->isEncrypted()); + + $zipFile->unchangeEntry('file 1'); + + self::assertEquals($zipFile['file 1'], 'content 1'); + self::assertFalse($zipFile->getEntryInfo('file 1')->isEncrypted()); + + self::assertEquals($zipFile['file 2'], 'content 2'); + self::assertFalse($zipFile->getEntryInfo('file 2')->isEncrypted()); + $zipFile->close(); + } + /** * Test support ZIP64 ext (slow test - normal). * Create > 65535 files in archive and open and extract to /dev/null. @@ -1830,10 +2117,12 @@ class ZipFileTest extends ZipTestCase $zipFile->openFile($this->outputFilename); self::assertEquals($zipFile->count(), $countFiles); + $i = 0; foreach ($zipFile as $entry => $content) { - + self::assertEquals($entry, $i . '.txt'); + self::assertEquals($content, $i); + $i++; } $zipFile->close(); } - -} \ No newline at end of file +} diff --git a/tests/PhpZip/ZipMatcherTest.php b/tests/PhpZip/ZipMatcherTest.php new file mode 100644 index 0000000..c824765 --- /dev/null +++ b/tests/PhpZip/ZipMatcherTest.php @@ -0,0 +1,111 @@ +matcher(); + self::assertInstanceOf(ZipEntryMatcher::class, $matcher); + + $this->assertTrue(is_array($matcher->getMatches())); + $this->assertCount(0, $matcher); + + $matcher->add(1)->add(10)->add(20); + $this->assertCount(3, $matcher); + $this->assertEquals($matcher->getMatches(), ['1', '10', '20']); + + $matcher->delete(); + $this->assertCount(97, $zipFile); + $this->assertCount(0, $matcher); + + $matcher->match('~^[2][1-5]|[3][6-9]|40$~s'); + $this->assertCount(10, $matcher); + $actualMatches = [ + '21', '22', '23', '24', '25', + '36', '37', '38', '39', + '40' + ]; + $this->assertEquals($matcher->getMatches(), $actualMatches); + $matcher->setPassword('qwerty'); + $info = $zipFile->getAllInfo(); + array_walk($info, function (ZipInfo $zipInfo) use ($actualMatches) { + self::assertEquals($zipInfo->isEncrypted(), in_array($zipInfo->getName(), $actualMatches)); + }); + + $matcher->all(); + $this->assertCount(count($zipFile), $matcher); + + $expectedNames = []; + $matcher->invoke(function ($entryName) use (&$expectedNames) { + $expectedNames[] = $entryName; + }); + $this->assertEquals($expectedNames, $matcher->getMatches()); + + $zipFile->close(); + } + + public function testDocsExample() + { + $zipFile = new ZipFile(); + for ($i = 0; $i < 100; $i++) { + $zipFile['file_'.$i.'.jpg'] = CryptoUtil::randomBytes(100); + } + + $renameEntriesArray = [ + 'file_10.jpg', + 'file_11.jpg', + 'file_12.jpg', + 'file_13.jpg', + 'file_14.jpg', + 'file_15.jpg', + 'file_16.jpg', + 'file_17.jpg', + 'file_18.jpg', + 'file_19.jpg', + 'file_50.jpg', + 'file_51.jpg', + 'file_52.jpg', + 'file_53.jpg', + 'file_54.jpg', + 'file_55.jpg', + 'file_56.jpg', + 'file_57.jpg', + 'file_58.jpg', + 'file_59.jpg', + ]; + + foreach ($renameEntriesArray as $name) { + self::assertTrue(isset($zipFile[$name])); + } + + $matcher = $zipFile->matcher(); + $matcher->match('~^file_(1|5)\d+~'); + self::assertEquals($matcher->getMatches(), $renameEntriesArray); + + $matcher->invoke(function ($entryName) use ($zipFile) { + $newName = preg_replace('~\.(jpe?g)$~i', '.no_optimize.$1', $entryName); + $zipFile->rename($entryName, $newName); + }); + + foreach ($renameEntriesArray as $name) { + self::assertFalse(isset($zipFile[$name])); + + $pathInfo = pathinfo($name); + $newName = $pathInfo['filename'].'.no_optimize.'.$pathInfo['extension']; + self::assertTrue(isset($zipFile[$newName])); + } + + $zipFile->close(); + } +} diff --git a/tests/PhpZip/ZipPasswordTest.php b/tests/PhpZip/ZipPasswordTest.php new file mode 100644 index 0000000..014b6b6 --- /dev/null +++ b/tests/PhpZip/ZipPasswordTest.php @@ -0,0 +1,330 @@ +addDir(__DIR__); + $zipFile->setPassword($password, ZipFileInterface::ENCRYPTION_METHOD_TRADITIONAL); + $zipFile->saveAsFile($this->outputFilename); + $zipFile->close(); + + self::assertCorrectZipArchive($this->outputFilename, $password); + + // check bad password for ZipCrypto + $zipFile->openFile($this->outputFilename); + $zipFile->setReadPassword($badPassword); + foreach ($zipFile->getListFiles() as $entryName) { + try { + $zipFile[$entryName]; + self::fail("Expected Exception has not been raised."); + } catch (ZipAuthenticationException $ae) { + self::assertNotNull($ae); + } + } + + // check correct password for ZipCrypto + $zipFile->setReadPassword($password); + foreach ($zipFile->getAllInfo() as $info) { + self::assertTrue($info->isEncrypted()); + self::assertContains('ZipCrypto', $info->getMethodName()); + $decryptContent = $zipFile[$info->getName()]; + self::assertNotEmpty($decryptContent); + self::assertContains('setPassword($password, ZipFileInterface::ENCRYPTION_METHOD_WINZIP_AES); + $zipFile->saveAsFile($this->outputFilename); + $zipFile->close(); + + self::assertCorrectZipArchive($this->outputFilename, $password); + + // check from WinZip AES encryption + $zipFile->openFile($this->outputFilename); + // set bad password WinZip AES + $zipFile->setReadPassword($badPassword); + foreach ($zipFile->getListFiles() as $entryName) { + try { + $zipFile[$entryName]; + self::fail("Expected Exception has not been raised."); + } catch (ZipAuthenticationException $ae) { + self::assertNotNull($ae); + } + } + + // set correct password WinZip AES + $zipFile->setReadPassword($password); + foreach ($zipFile->getAllInfo() as $info) { + self::assertTrue($info->isEncrypted()); + self::assertContains('WinZip', $info->getMethodName()); + $decryptContent = $zipFile[$info->getName()]; + self::assertNotEmpty($decryptContent); + self::assertContains('addFromString('file1', ''); + $zipFile->removePassword(); + $zipFile->addFromString('file2', ''); + $zipFile->saveAsFile($this->outputFilename); + $zipFile->close(); + + self::assertCorrectZipArchive($this->outputFilename); + + // check remove password + $zipFile->openFile($this->outputFilename); + foreach ($zipFile->getAllInfo() as $info) { + self::assertFalse($info->isEncrypted()); + } + $zipFile->close(); + } + + public function testTraditionalEncryption() + { + $password = base64_encode(CryptoUtil::randomBytes(50)); + + $zip = new ZipFile(); + $zip->addDirRecursive($this->outputDirname); + $zip->setPassword($password, ZipFileInterface::ENCRYPTION_METHOD_TRADITIONAL); + $zip->saveAsFile($this->outputFilename); + $zip->close(); + + self::assertCorrectZipArchive($this->outputFilename, $password); + + $zip->openFile($this->outputFilename); + $zip->setReadPassword($password); + self::assertFilesResult($zip, array_keys(self::$files)); + foreach ($zip->getAllInfo() as $info) { + if (!$info->isFolder()) { + self::assertTrue($info->isEncrypted()); + self::assertContains('ZipCrypto', $info->getMethodName()); + } + } + $zip->close(); + } + + /** + * @dataProvider winZipKeyStrengthProvider + * @param int $encryptionMethod + * @param int $bitSize + */ + public function testWinZipAesEncryption($encryptionMethod, $bitSize) + { + $password = base64_encode(CryptoUtil::randomBytes(50)); + + $zip = new ZipFile(); + $zip->addDirRecursive($this->outputDirname); + $zip->setPassword($password, $encryptionMethod); + $zip->saveAsFile($this->outputFilename); + $zip->close(); + + self::assertCorrectZipArchive($this->outputFilename, $password); + + $zip->openFile($this->outputFilename); + $zip->setReadPassword($password); + self::assertFilesResult($zip, array_keys(self::$files)); + foreach ($zip->getAllInfo() as $info) { + if (!$info->isFolder()) { + self::assertTrue($info->isEncrypted()); + self::assertEquals($info->getEncryptionMethod(), $encryptionMethod); + self::assertContains('WinZip AES-' . $bitSize, $info->getMethodName()); + } + } + $zip->close(); + } + + /** + * @return array + */ + public function winZipKeyStrengthProvider() + { + return [ + [ZipFileInterface::ENCRYPTION_METHOD_WINZIP_AES_128, 128], + [ZipFileInterface::ENCRYPTION_METHOD_WINZIP_AES_192, 192], + [ZipFileInterface::ENCRYPTION_METHOD_WINZIP_AES, 256], + [ZipFileInterface::ENCRYPTION_METHOD_WINZIP_AES_256, 256], + ]; + } + + public function testEncryptionEntries() + { + $password1 = '353442434235424234'; + $password2 = 'adgerhvrwjhqqehtqhkbqrgewg'; + + $zip = new ZipFile(); + $zip->addDir($this->outputDirname); + $zip->setPasswordEntry('.hidden', $password1, ZipFileInterface::ENCRYPTION_METHOD_TRADITIONAL); + $zip->setPasswordEntry('text file.txt', $password2, ZipFileInterface::ENCRYPTION_METHOD_WINZIP_AES); + $zip->saveAsFile($this->outputFilename); + $zip->close(); + + $zip->openFile($this->outputFilename); + $zip->setReadPasswordEntry('.hidden', $password1); + $zip->setReadPasswordEntry('text file.txt', $password2); + self::assertFilesResult($zip, [ + '.hidden', + 'text file.txt', + 'Текстовый документ.txt', + 'empty dir/', + ]); + + $info = $zip->getEntryInfo('.hidden'); + self::assertTrue($info->isEncrypted()); + self::assertContains('ZipCrypto', $info->getMethodName()); + + $info = $zip->getEntryInfo('text file.txt'); + self::assertTrue($info->isEncrypted()); + self::assertContains('WinZip AES', $info->getMethodName()); + + self::assertFalse($zip->getEntryInfo('Текстовый документ.txt')->isEncrypted()); + self::assertFalse($zip->getEntryInfo('empty dir/')->isEncrypted()); + + $zip->close(); + } + + public function testEncryptionEntriesWithDefaultPassword() + { + $password1 = '353442434235424234'; + $password2 = 'adgerhvrwjhqqehtqhkbqrgewg'; + $defaultPassword = ' f f f f f ffff f5 '; + + $zip = new ZipFile(); + $zip->addDir($this->outputDirname); + $zip->setPassword($defaultPassword); + $zip->setPasswordEntry('.hidden', $password1, ZipFileInterface::ENCRYPTION_METHOD_TRADITIONAL); + $zip->setPasswordEntry('text file.txt', $password2, ZipFileInterface::ENCRYPTION_METHOD_WINZIP_AES); + $zip->saveAsFile($this->outputFilename); + $zip->close(); + + $zip->openFile($this->outputFilename); + $zip->setReadPassword($defaultPassword); + $zip->setReadPasswordEntry('.hidden', $password1); + $zip->setReadPasswordEntry('text file.txt', $password2); + self::assertFilesResult($zip, [ + '.hidden', + 'text file.txt', + 'Текстовый документ.txt', + 'empty dir/', + ]); + + $info = $zip->getEntryInfo('.hidden'); + self::assertTrue($info->isEncrypted()); + self::assertContains('ZipCrypto', $info->getMethodName()); + + $info = $zip->getEntryInfo('text file.txt'); + self::assertTrue($info->isEncrypted()); + self::assertContains('WinZip AES', $info->getMethodName()); + + $info = $zip->getEntryInfo('Текстовый документ.txt'); + self::assertTrue($info->isEncrypted()); + self::assertContains('WinZip AES', $info->getMethodName()); + + self::assertFalse($zip->getEntryInfo('empty dir/')->isEncrypted()); + + $zip->close(); + } + + /** + * @expectedException \PhpZip\Exception\ZipException + * @expectedExceptionMessage Invalid encryption method + */ + public function testSetEncryptionMethodInvalid() + { + $zipFile = new ZipFile(); + $encryptionMethod = 9999; + $zipFile->setPassword('pass', $encryptionMethod); + $zipFile['entry'] = 'content'; + $zipFile->outputAsString(); + } + + public function testEntryPassword() + { + $zipFile = new ZipFile(); + $zipFile->setPassword('pass'); + $zipFile['file'] = 'content'; + self::assertFalse($zipFile->getEntryInfo('file')->isEncrypted()); + for ($i = 1; $i <= 10; $i++) { + $zipFile['file' . $i] = 'content'; + if ($i < 6) { + $zipFile->setPasswordEntry('file' . $i, 'pass'); + self::assertTrue($zipFile->getEntryInfo('file' . $i)->isEncrypted()); + } else { + self::assertFalse($zipFile->getEntryInfo('file' . $i)->isEncrypted()); + } + } + $zipFile->removePasswordEntry('file3'); + self::assertFalse($zipFile->getEntryInfo('file3')->isEncrypted()); + self::asserttrue($zipFile->getEntryInfo('file2')->isEncrypted()); + $zipFile->removePassword(); + $infoList = $zipFile->getAllInfo(); + array_walk($infoList, function (ZipInfo $zipInfo) { + self::assertFalse($zipInfo->isEncrypted()); + }); + $zipFile->close(); + } + + /** + * @expectedException \PhpZip\Exception\ZipException + * @expectedExceptionMessage Invalid encryption method + */ + public function testInvalidEncryptionMethodEntry() + { + $zipFile = new ZipFile(); + $zipFile->addFromString('file', 'content', ZipFileInterface::METHOD_STORED); + $zipFile->setPasswordEntry('file', 'pass', 99); + } + + public function testArchivePasswordUpdateWithoutSetReadPassword() + { + $zipFile = new ZipFile(); + $zipFile['file1'] = 'content'; + $zipFile['file2'] = 'content'; + $zipFile['file3'] = 'content'; + $zipFile->setPassword('password'); + $zipFile->saveAsFile($this->outputFilename); + $zipFile->close(); + + self::assertCorrectZipArchive($this->outputFilename, 'password'); + + $zipFile->openFile($this->outputFilename); + self::assertCount(3, $zipFile); + foreach ($zipFile->getAllInfo() as $info) { + self::assertTrue($info->isEncrypted()); + } + unset($zipFile['file3']); + $zipFile['file4'] = 'content'; + $zipFile->rewrite(); + + self::assertCorrectZipArchive($this->outputFilename, 'password'); + + self::assertCount(3, $zipFile); + self::assertFalse(isset($zipFile['file3'])); + self::assertTrue(isset($zipFile['file4'])); + self::assertTrue($zipFile->getEntryInfo('file1')->isEncrypted()); + self::assertTrue($zipFile->getEntryInfo('file2')->isEncrypted()); + self::assertFalse($zipFile->getEntryInfo('file4')->isEncrypted()); + self::assertEquals($zipFile['file4'], 'content'); + + $zipFile->extractTo($this->outputDirname, ['file4']); + + self::assertTrue(file_exists($this->outputDirname . DIRECTORY_SEPARATOR . 'file4')); + self::assertEquals(file_get_contents($this->outputDirname . DIRECTORY_SEPARATOR . 'file4'), $zipFile['file4']); + + $zipFile->close(); + } +} diff --git a/tests/PhpZip/ZipTestCase.php b/tests/PhpZip/ZipTestCase.php index 8fcb8d8..6de8537 100644 --- a/tests/PhpZip/ZipTestCase.php +++ b/tests/PhpZip/ZipTestCase.php @@ -1,4 +1,5 @@ outputFilename = sys_get_temp_dir() . '/' . $id . '.zip'; - $this->outputDirname = sys_get_temp_dir() . '/' . $id; + $tempDir = sys_get_temp_dir() . '/phpunit-phpzip'; + if (!is_dir($tempDir)) { + if (!mkdir($tempDir, 0755, true)) { + throw new \RuntimeException("Dir " . $tempDir . " can't created"); + } + } + $this->outputFilename = $tempDir . '/' . $id . '.zip'; + $this->outputDirname = $tempDir . '/' . $id; } /** @@ -118,12 +125,13 @@ class ZipTestCase extends \PHPUnit_Framework_TestCase { if (DIRECTORY_SEPARATOR !== '\\' && `which zipalign`) { exec("zipalign -c -v 4 " . escapeshellarg($filename), $output, $returnCode); - if ($showErrors && $returnCode !== 0) fwrite(STDERR, implode(PHP_EOL, $output)); + if ($showErrors && $returnCode !== 0) { + fwrite(STDERR, implode(PHP_EOL, $output)); + } return $returnCode === 0; } else { - fwrite(STDERR, 'Can not find program "zipalign" for test'); + fwrite(STDERR, 'Can not find program "zipalign" for test' . PHP_EOL); return null; } } - -} \ No newline at end of file +} diff --git a/tests/PhpZip/php-zip-ext-test-resources/binarynull.zip b/tests/PhpZip/php-zip-ext-test-resources/binarynull.zip new file mode 100644 index 0000000000000000000000000000000000000000..9da004efed082ebb572404a893dc32ee6f140ccd GIT binary patch literal 656 zcmWIWW@Zs#U|`^2sA;M*nY?a~`7cHWhV4uY44e$23?-?>C3;x}sp+91oD9r7FST97 zfjG2+h2aJB3+CVTkN-2XG3uN9o3pvOv9Y-+r2P2bfAE~;iK~W3>`!
9E#W5Eumld{?zcXnRUJkk+qI2+=x$rr)yM71@{vF@vd8N*e
z
$28?CZ_vMY?gnZ3mabJl>F>FYBv51
z|0*KcR5|DBBc^(>&vRT8L(QcnO1&^xF&U|=eZ|w4-@pujbcOl^F52(gf38O@PX2wP
z?L&~)6^H=m-~X!J;sq)!kn{C8QHu(Z?g08F<;^vMI0~xQ^=7JR`-iu`IIINsEwvLAtE_^*t#na-|i?|8&uG-ZHg`Lj8K_(Js{C<*e
z;Kol!4cZ$_7mNs8j+#!}XA@G5sq!i;h0@0zQ{YH-i~2ba!Tt7z7S1{#M#vm^`yVej
zVt4L0&t<+Pr=+>Zcl&4LN!#puVU1V-z?>`Rr<9%JW*?dTz9v}XOu5n9q-@t`^ufNP
zs(5U^KCqja!bIprlT{1j1%G)G3n2vNV*Y%
1>zAi0~;
z!5ND@*s2@rDBx^1{A{z#>^R+mVz0<*s7yQgYWL9ztd4ruBY=oI28&7FBB{O17=;PL
z)_38N3=K-*xz0;$*9LeI_-y%Jdng=ECu>fRha4@Sd+=f5i-Yp)vJcKwfL!~=$jFTD
z`i+@(Anjr3Fs!u+X_~vh80C_r4D8MyV|~}OV)11wpeD$^M9j&uO_02il59^&Ie#1b
zYEv%PZy8A7uFNrh+MA!tU$~e3gvZTxN7KHPRTYj=^9+mj?ZW$NbB`epD;z&Ylm0Lr
z@n1>PCaN1e)Fib&V_QKrvQus3H*d^d4GT7rCqF@KF^fzk#*{ND&?dkOZ#EZ=AaT<_
zF)So0xXFl|`aT(tVvV9-(qaXEN@{cbXIzBXZXG)dL0vz&aJqUjP+zR0o~P0i5Sv)d
z+RQ-WE1dmkCXB$R5p$mPO-iu&x@;HO7i^`CHW`tdSJ%O&`4g-;GB;<
zFCCljQIL~o6=WCi`YK+^E2)vZ5YVGL$?}R}Z6(;=5h}7Bv5_F<)whwna0q6-EUzJ~
z{VFjK&z2eFKVawQBwA h#U)>4
63Z+K+?WRTM
z9G1?6gfxm+Oxj%f2eq`Q+NNwc~8X
zSoT#RmVN1!Pp`UI4m-fg9OLI4(AXBw4YI8d%igvdYggOy>%B0K{0H>`EUz}h^6Exn
znU|THb2dpX#xly!W0}`N9?M||#8^fi?5L}qIhI-a>fiv|e2({{!LqN4VcD1YB~*s)
zBn;$fI+lG^0+w0249iURiYJ3@|4J-t=KS?oK2O@w`KA-(*6)QXoDDktyWk@hJGUdf
zcb@m|UwPy*-$vmxHECbU+CF0?)gQfx;(LonR9eC^FPYjigpSFvjVWU7!fwAM!LlFR
zHwMds%$&a>kzDHIO10qT`!0*yTO!g(80F#gVn-eAqzA^QeL#rw_0%=SNC=DI?1KIE
zE$!`kaiMrETsFm&3*B?bhTNt5+iG_AvQv_gHqJ*`nAL)i(96v4__S9q!q(8;Rc5we
zaqxX7Zm%#5jc&uTaLt^HgAH-UJsKdLst=2d7~(UO;pww}fmnx}={^zRe#f}