mirror of
https://github.com/Ne-Lexa/php-zip.git
synced 2025-10-18 16:46:26 +02:00
607 lines
23 KiB
PHP
607 lines
23 KiB
PHP
<?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;
|
|
}
|
|
}
|