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); // zip align if ( $this->zipModel->isZipAlign() && !$entry->isEncrypted() && $entry->getMethod() === ZipFileInterface::METHOD_STORED ) { $dataAlignmentMultiple = $this->zipModel->getZipAlign(); if (StringUtil::endsWith($entry->getName(), '.so')) { $dataAlignmentMultiple = ApkAlignmentExtraField::ANDROID_COMMON_PAGE_ALIGNMENT_BYTES; } $dataMinStartOffset = $offset + ZipEntry::LOCAL_FILE_HEADER_MIN_LEN + $extraLength + $nameLength + ApkAlignmentExtraField::ALIGNMENT_ZIP_EXTRA_MIN_SIZE_BYTES; $padding = ($dataAlignmentMultiple - ($dataMinStartOffset % $dataAlignmentMultiple)) % $dataAlignmentMultiple; $alignExtra = new ApkAlignmentExtraField(); $alignExtra->setMultiple($dataAlignmentMultiple); $alignExtra->setPadding($padding); $extraFieldsCollection = clone $entry->getExtraFieldsCollection(); $extraFieldsCollection->add($alignExtra); $extra = ExtraFieldsFactory::createSerializedData($extraFieldsCollection); $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)" ); } $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 ) ); if ($nameLength > 0) { fwrite($this->out, $entry->getName()); } if ($extraLength > 0) { fwrite($this->out, $extra); } 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); $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) { /** * @var WinZipAesEntryExtraField $field */ $field = $extraFieldsCollection->get(WinZipAesEntryExtraField::getHeaderId()); if (null !== $field) { $method = $field->getMethod(); } } 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 (in_array($entry->getEncryptionMethod(), [ ZipFileInterface::ENCRYPTION_METHOD_WINZIP_AES_128, ZipFileInterface::ENCRYPTION_METHOD_WINZIP_AES_192, ZipFileInterface::ENCRYPTION_METHOD_WINZIP_AES_256, ], true)) { $keyStrength = WinZipAesEntryExtraField::getKeyStrangeFromEncryptionMethod($entry->getEncryptionMethod()); // size bits $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); $entry->setCrc(0); } $extraFieldsCollection->add($field); $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 * @throws ZipException */ 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; } }