| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606 |
- <?php
- namespace PhpZip\Stream;
- use PhpZip\Crypto\TraditionalPkwareEncryptionEngine;
- use PhpZip\Crypto\WinZipAesEngine;
- use PhpZip\Exception\InvalidArgumentException;
- use PhpZip\Exception\RuntimeException;
- use PhpZip\Exception\ZipException;
- use PhpZip\Extra\ExtraFieldsFactory;
- use PhpZip\Extra\Fields\ApkAlignmentExtraField;
- use PhpZip\Extra\Fields\WinZipAesEntryExtraField;
- use PhpZip\Extra\Fields\Zip64ExtraField;
- use PhpZip\Model\EndOfCentralDirectory;
- use PhpZip\Model\Entry\OutputOffsetEntry;
- use PhpZip\Model\Entry\ZipChangesEntry;
- use PhpZip\Model\Entry\ZipSourceEntry;
- use PhpZip\Model\ZipEntry;
- use PhpZip\Model\ZipModel;
- use PhpZip\Util\PackUtil;
- use PhpZip\Util\StringUtil;
- use PhpZip\ZipFile;
- /**
- * Write zip file.
- *
- * @author Ne-Lexa alexey@nelexa.ru
- * @license MIT
- */
- class ZipOutputStream implements ZipOutputStreamInterface
- {
- /** @var resource */
- protected $out;
- /** @var ZipModel */
- protected $zipModel;
- /**
- * ZipOutputStream constructor.
- *
- * @param resource $out
- * @param ZipModel $zipModel
- */
- public function __construct($out, ZipModel $zipModel)
- {
- if (!\is_resource($out)) {
- throw new InvalidArgumentException('$out must be resource');
- }
- $this->out = $out;
- $this->zipModel = $zipModel;
- }
- /**
- * @throws ZipException
- */
- 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() === ZipFile::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 ($size > 0xffff) {
- 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->getExtractedOS() << 8) | $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 ($entryContent !== null) {
- fwrite($this->out, $entryContent);
- }
- if ($entry->getCrc() === ZipEntry::UNKNOWN) {
- throw new ZipException(sprintf('No crc for entry %s', $entry->getName()));
- }
- if ($entry->getSize() === ZipEntry::UNKNOWN) {
- throw new ZipException(sprintf('No uncompressed size for entry %s', $entry->getName()));
- }
- if ($entry->getCompressedSize() === ZipEntry::UNKNOWN) {
- throw new ZipException(sprintf('No compressed size for entry %s', $entry->getName()));
- }
- 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 ($compressedSize !== $entry->getCompressedSize()) {
- throw new ZipException(
- $entry->getName() . ' (expected compressed entry size of '
- . $entry->getCompressedSize() . ' bytes, ' .
- 'but is actually ' . $compressedSize . ' bytes)'
- );
- }
- }
- /**
- * @param ZipEntry $entry
- *
- * @throws ZipException
- *
- * @return string|null
- */
- protected function entryCommitChangesAndReturnContent(ZipEntry $entry)
- {
- if ($entry->getCreatedOS() === ZipEntry::UNKNOWN) {
- $entry->setCreatedOS(ZipEntry::PLATFORM_UNIX);
- }
- if ($entry->getSoftwareVersion() === ZipEntry::UNKNOWN) {
- $entry->setSoftwareVersion(63);
- }
- if ($entry->getExtractedOS() === ZipEntry::UNKNOWN) {
- $entry->setExtractedOS(ZipEntry::PLATFORM_UNIX);
- }
- if ($entry->getTime() === ZipEntry::UNKNOWN) {
- $entry->setTime(time());
- }
- $method = $entry->getMethod();
- $encrypted = $entry->isEncrypted();
- // See appendix D of PKWARE's ZIP File Format Specification.
- $utf8 = true;
- if ($encrypted && $entry->getPassword() === null) {
- throw new ZipException(sprintf('Password not set for entry %s', $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 && $method === ZipEntry::METHOD_WINZIP_AES) {
- /**
- * @var WinZipAesEntryExtraField $field
- */
- $field = $extraFieldsCollection->get(WinZipAesEntryExtraField::getHeaderId());
- if ($field !== null) {
- $method = $field->getMethod();
- }
- }
- switch ($method) {
- case ZipFile::METHOD_STORED:
- break;
- case ZipFile::METHOD_DEFLATED:
- $entryContent = gzdeflate($entryContent, $entry->getCompressionLevel());
- break;
- case ZipFile::METHOD_BZIP2:
- $compressionLevel = $entry->getCompressionLevel() === ZipFile::LEVEL_DEFAULT_COMPRESSION ?
- ZipEntry::LEVEL_DEFAULT_BZIP2_COMPRESSION :
- $entry->getCompressionLevel();
- /** @noinspection PhpComposerExtensionStubsInspection */
- $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 ($method === ZipFile::METHOD_DEFLATED) {
- $bit1 = false;
- $bit2 = false;
- switch ($entry->getCompressionLevel()) {
- case ZipFile::LEVEL_BEST_COMPRESSION:
- $bit1 = true;
- break;
- case ZipFile::LEVEL_FAST:
- $bit2 = true;
- break;
- case ZipFile::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(),
- [
- ZipFile::ENCRYPTION_METHOD_WINZIP_AES_128,
- ZipFile::ENCRYPTION_METHOD_WINZIP_AES_192,
- ZipFile::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 ($size >= 20 && $method !== ZipFile::METHOD_BZIP2) {
- $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() === ZipFile::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
- *
- * @throws ZipException
- *
- * @return string
- */
- protected function determineBestCompressionMethod(ZipEntry $entry, $content)
- {
- if ($content !== null) {
- $entryContent = gzdeflate($content, $entry->getCompressionLevel());
- if (\strlen($entryContent) < \strlen($content)) {
- $entry->setMethod(ZipFile::METHOD_DEFLATED);
- return $entryContent;
- }
- $entry->setMethod(ZipFile::METHOD_STORED);
- }
- return $content;
- }
- /**
- * Writes a Central File Header record.
- *
- * @param OutputOffsetEntry $outEntry
- */
- 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 (($compressedSize | $size) === ZipEntry::UNKNOWN) {
- 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->getCreatedOS() << 8) | $entry->getSoftwareVersion(),
- // version needed to extract 2 bytes
- ($entry->getExtractedOS() << 8) | $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
- $entry->getInternalAttributes(),
- // 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 ($extraSize > 0) {
- // extra field (variable size)
- fwrite($this->out, $extra);
- }
- if ($commentLength > 0) {
- // file comment (variable size)
- fwrite($this->out, $entry->getComment());
- }
- }
- /**
- * @param int $centralDirectoryOffset
- */
- protected function writeEndOfCentralDirectoryRecord($centralDirectoryOffset)
- {
- $cdEntriesCount = \count($this->zipModel);
- $position = ftell($this->out);
- $centralDirectorySize = $position - $centralDirectoryOffset;
- $cdEntriesZip64 = $cdEntriesCount > 0xFFFF;
- $cdSizeZip64 = $centralDirectorySize > 0xFFFFFFFF;
- $cdOffsetZip64 = $centralDirectoryOffset > 0xFFFFFFFF;
- $zip64Required = $cdEntriesZip64 || $cdSizeZip64 || $cdOffsetZip64;
- if ($zip64Required) {
- $zip64EndOfCentralDirectoryOffset = ftell($this->out);
- // find max software version, version needed to extract and most common platform
- list($softwareVersion, $versionNeededToExtract) = array_reduce(
- $this->zipModel->getEntries(),
- static function (array $carry, ZipEntry $entry) {
- $carry[0] = max($carry[0], $entry->getSoftwareVersion() & 0xFF);
- $carry[1] = max($carry[1], $entry->getVersionNeededToExtract() & 0xFF);
- return $carry;
- },
- [10 /* simple file min ver */, 45 /* zip64 ext min ver */]
- );
- $createdOS = $extractedOS = ZipEntry::PLATFORM_FAT;
- $versionMadeBy = ($createdOS << 8) | max($softwareVersion, 45 /* zip64 ext min ver */);
- $versionExtractedBy = ($extractedOS << 8) | max($versionNeededToExtract, 45 /* zip64 ext min ver */);
- // signature 4 bytes (0x06064b50)
- fwrite($this->out, pack('V', EndOfCentralDirectory::ZIP64_END_OF_CD_RECORD_SIG));
- // size of zip64 end of central
- // directory record 8 bytes
- fwrite($this->out, PackUtil::packLongLE(44));
- fwrite(
- $this->out,
- pack(
- 'vvVV',
- // version made by 2 bytes
- $versionMadeBy & 0xFFFF,
- // version needed to extract 2 bytes
- $versionExtractedBy & 0xFFFF,
- // number of this disk 4 bytes
- 0,
- // number of the disk with the
- // start of the central directory 4 bytes
- 0
- )
- );
- // total number of entries in the
- // central directory on this disk 8 bytes
- fwrite($this->out, PackUtil::packLongLE($cdEntriesCount));
- // total number of entries in the
- // central directory 8 bytes
- fwrite($this->out, PackUtil::packLongLE($cdEntriesCount));
- // 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));
- // write zip64 end of central directory locator
- fwrite(
- $this->out,
- pack(
- 'VV',
- // zip64 end of central dir locator
- // signature 4 bytes (0x07064b50)
- EndOfCentralDirectory::ZIP64_END_OF_CD_LOCATOR_SIG,
- // number of the disk with the
- // start of the zip64 end of
- // central directory 4 bytes
- 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 = $comment !== null ? \strlen($comment) : 0;
- fwrite(
- $this->out,
- pack(
- 'VvvvvVVv',
- // end of central dir signature 4 bytes (0x06054b50)
- EndOfCentralDirectory::END_OF_CD_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
- $cdEntriesZip64 ? 0xFFFF : $cdEntriesCount,
- // total number of entries in
- // the central directory 2 bytes
- $cdEntriesZip64 ? 0xFFFF : $cdEntriesCount,
- // size of the central directory 4 bytes
- $cdSizeZip64 ? 0xFFFFFFFF : $centralDirectorySize,
- // offset of start of central
- // directory with respect to
- // the starting disk number 4 bytes
- $cdOffsetZip64 ? 0xFFFFFFFF : $centralDirectoryOffset,
- // .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;
- }
- }
|