2019-12-30 18:47:37 +03:00
|
|
|
<?php
|
|
|
|
|
2021-02-22 13:12:01 +03:00
|
|
|
declare(strict_types=1);
|
|
|
|
|
|
|
|
/*
|
|
|
|
* This file is part of the nelexa/zip package.
|
|
|
|
* (c) Ne-Lexa <https://github.com/Ne-Lexa/php-zip>
|
|
|
|
* For the full copyright and license information, please view the LICENSE
|
|
|
|
* file that was distributed with this source code.
|
|
|
|
*/
|
|
|
|
|
2019-12-30 18:47:37 +03:00
|
|
|
namespace PhpZip\IO;
|
|
|
|
|
|
|
|
use PhpZip\Constants\DosCodePage;
|
|
|
|
use PhpZip\Constants\ZipCompressionMethod;
|
|
|
|
use PhpZip\Constants\ZipConstants;
|
|
|
|
use PhpZip\Constants\ZipEncryptionMethod;
|
|
|
|
use PhpZip\Constants\ZipPlatform;
|
|
|
|
use PhpZip\Constants\ZipVersion;
|
|
|
|
use PhpZip\Exception\ZipException;
|
|
|
|
use PhpZip\Exception\ZipUnsupportMethodException;
|
2020-01-06 11:53:17 +03:00
|
|
|
use PhpZip\IO\Filter\Cipher\Pkware\PKEncryptionStreamFilter;
|
2019-12-30 18:47:37 +03:00
|
|
|
use PhpZip\IO\Filter\Cipher\WinZipAes\WinZipAesEncryptionStreamFilter;
|
|
|
|
use PhpZip\Model\Data\ZipSourceFileData;
|
|
|
|
use PhpZip\Model\Extra\Fields\WinZipAesExtraField;
|
|
|
|
use PhpZip\Model\Extra\Fields\Zip64ExtraField;
|
|
|
|
use PhpZip\Model\ZipContainer;
|
|
|
|
use PhpZip\Model\ZipEntry;
|
|
|
|
|
|
|
|
class ZipWriter
|
|
|
|
{
|
|
|
|
/** @var int Chunk read size */
|
2021-02-22 13:12:01 +03:00
|
|
|
public const CHUNK_SIZE = 8192;
|
2019-12-30 18:47:37 +03:00
|
|
|
|
2021-02-22 13:12:01 +03:00
|
|
|
protected ZipContainer $zipContainer;
|
2019-12-30 18:47:37 +03:00
|
|
|
|
|
|
|
public function __construct(ZipContainer $container)
|
|
|
|
{
|
2020-01-21 14:10:47 +03:00
|
|
|
// we clone the container so that the changes made to
|
|
|
|
// it do not affect the data in the ZipFile class
|
|
|
|
$this->zipContainer = clone $container;
|
2019-12-30 18:47:37 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param resource $outStream
|
|
|
|
*
|
|
|
|
* @throws ZipException
|
|
|
|
*/
|
2021-02-22 13:12:01 +03:00
|
|
|
public function write($outStream): void
|
2019-12-30 18:47:37 +03:00
|
|
|
{
|
|
|
|
if (!\is_resource($outStream)) {
|
|
|
|
throw new \InvalidArgumentException('$outStream must be resource');
|
|
|
|
}
|
|
|
|
$this->beforeWrite();
|
|
|
|
$this->writeLocalBlock($outStream);
|
|
|
|
$cdOffset = ftell($outStream);
|
|
|
|
$this->writeCentralDirectoryBlock($outStream);
|
|
|
|
$cdSize = ftell($outStream) - $cdOffset;
|
|
|
|
$this->writeEndOfCentralDirectoryBlock($outStream, $cdOffset, $cdSize);
|
|
|
|
}
|
|
|
|
|
2021-02-22 13:12:01 +03:00
|
|
|
protected function beforeWrite(): void
|
2019-12-30 18:47:37 +03:00
|
|
|
{
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param resource $outStream
|
|
|
|
*
|
|
|
|
* @throws ZipException
|
|
|
|
*/
|
2021-02-22 13:12:01 +03:00
|
|
|
protected function writeLocalBlock($outStream): void
|
2019-12-30 18:47:37 +03:00
|
|
|
{
|
|
|
|
$zipEntries = $this->zipContainer->getEntries();
|
|
|
|
|
|
|
|
foreach ($zipEntries as $zipEntry) {
|
|
|
|
$this->writeLocalHeader($outStream, $zipEntry);
|
|
|
|
$this->writeData($outStream, $zipEntry);
|
|
|
|
|
|
|
|
if ($zipEntry->isDataDescriptorEnabled()) {
|
|
|
|
$this->writeDataDescriptor($outStream, $zipEntry);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param resource $outStream
|
|
|
|
*
|
|
|
|
* @throws ZipException
|
|
|
|
*/
|
2021-02-22 13:12:01 +03:00
|
|
|
protected function writeLocalHeader($outStream, ZipEntry $entry): void
|
2019-12-30 18:47:37 +03:00
|
|
|
{
|
|
|
|
$relativeOffset = ftell($outStream);
|
|
|
|
$entry->setLocalHeaderOffset($relativeOffset);
|
|
|
|
|
|
|
|
if ($entry->isEncrypted() && $entry->getEncryptionMethod() === ZipEncryptionMethod::PKWARE) {
|
|
|
|
$entry->enableDataDescriptor(true);
|
|
|
|
}
|
|
|
|
|
2021-02-22 13:12:01 +03:00
|
|
|
$dd = $entry->isDataDescriptorRequired()
|
|
|
|
|| $entry->isDataDescriptorEnabled();
|
2019-12-30 18:47:37 +03:00
|
|
|
|
|
|
|
$compressedSize = $entry->getCompressedSize();
|
|
|
|
$uncompressedSize = $entry->getUncompressedSize();
|
|
|
|
|
|
|
|
$entry->getLocalExtraFields()->remove(Zip64ExtraField::HEADER_ID);
|
|
|
|
|
|
|
|
if ($compressedSize > ZipConstants::ZIP64_MAGIC || $uncompressedSize > ZipConstants::ZIP64_MAGIC) {
|
|
|
|
$entry->getLocalExtraFields()->add(
|
|
|
|
new Zip64ExtraField($uncompressedSize, $compressedSize)
|
|
|
|
);
|
|
|
|
|
|
|
|
$compressedSize = ZipConstants::ZIP64_MAGIC;
|
|
|
|
$uncompressedSize = ZipConstants::ZIP64_MAGIC;
|
|
|
|
}
|
|
|
|
|
|
|
|
$compressionMethod = $entry->getCompressionMethod();
|
|
|
|
$crc = $entry->getCrc();
|
|
|
|
|
|
|
|
if ($entry->isEncrypted() && ZipEncryptionMethod::isWinZipAesMethod($entry->getEncryptionMethod())) {
|
|
|
|
/** @var WinZipAesExtraField|null $winZipAesExtra */
|
|
|
|
$winZipAesExtra = $entry->getLocalExtraField(WinZipAesExtraField::HEADER_ID);
|
|
|
|
|
|
|
|
if ($winZipAesExtra === null) {
|
|
|
|
$winZipAesExtra = WinZipAesExtraField::create($entry);
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($winZipAesExtra->isV2()) {
|
|
|
|
$crc = 0;
|
|
|
|
}
|
|
|
|
$compressionMethod = ZipCompressionMethod::WINZIP_AES;
|
|
|
|
}
|
|
|
|
|
|
|
|
$extra = $this->getExtraFieldsContents($entry, true);
|
|
|
|
$name = $entry->getName();
|
|
|
|
$dosCharset = $entry->getCharset();
|
|
|
|
|
|
|
|
if ($dosCharset !== null && !$entry->isUtf8Flag()) {
|
|
|
|
$name = DosCodePage::fromUTF8($name, $dosCharset);
|
|
|
|
}
|
|
|
|
|
|
|
|
$nameLength = \strlen($name);
|
|
|
|
$extraLength = \strlen($extra);
|
|
|
|
|
|
|
|
$size = $nameLength + $extraLength;
|
|
|
|
|
2021-12-12 12:50:30 +03:00
|
|
|
if ($size > 0xFFFF) {
|
2019-12-30 18:47:37 +03:00
|
|
|
throw new ZipException(
|
|
|
|
sprintf(
|
|
|
|
'%s (the total size of %s bytes for the name, extra fields and comment exceeds the maximum size of %d bytes)',
|
|
|
|
$entry->getName(),
|
|
|
|
$size,
|
2021-12-12 12:50:30 +03:00
|
|
|
0xFFFF
|
2019-12-30 18:47:37 +03:00
|
|
|
)
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
$extractedBy = ($entry->getExtractedOS() << 8) | $entry->getExtractVersion();
|
|
|
|
|
|
|
|
fwrite(
|
|
|
|
$outStream,
|
|
|
|
pack(
|
|
|
|
'VvvvVVVVvv',
|
|
|
|
// local file header signature 4 bytes (0x04034b50)
|
|
|
|
ZipConstants::LOCAL_FILE_HEADER,
|
|
|
|
// version needed to extract 2 bytes
|
|
|
|
$extractedBy,
|
|
|
|
// general purpose bit flag 2 bytes
|
|
|
|
$entry->getGeneralPurposeBitFlags(),
|
|
|
|
// compression method 2 bytes
|
|
|
|
$compressionMethod,
|
|
|
|
// last mod file time 2 bytes
|
|
|
|
// last mod file date 2 bytes
|
|
|
|
$entry->getDosTime(),
|
|
|
|
// crc-32 4 bytes
|
|
|
|
$dd ? 0 : $crc,
|
|
|
|
// compressed size 4 bytes
|
|
|
|
$dd ? 0 : $compressedSize,
|
|
|
|
// uncompressed size 4 bytes
|
|
|
|
$dd ? 0 : $uncompressedSize,
|
|
|
|
// file name length 2 bytes
|
|
|
|
$nameLength,
|
|
|
|
// extra field length 2 bytes
|
|
|
|
$extraLength
|
|
|
|
)
|
|
|
|
);
|
|
|
|
|
|
|
|
if ($nameLength > 0) {
|
|
|
|
fwrite($outStream, $name);
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($extraLength > 0) {
|
|
|
|
fwrite($outStream, $extra);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Merges the local file data fields of the given ZipExtraFields.
|
|
|
|
*
|
|
|
|
* @throws ZipException
|
|
|
|
*/
|
2021-02-22 13:12:01 +03:00
|
|
|
protected function getExtraFieldsContents(ZipEntry $entry, bool $local): string
|
2019-12-30 18:47:37 +03:00
|
|
|
{
|
2021-02-22 13:12:01 +03:00
|
|
|
$collection = $local
|
|
|
|
? $entry->getLocalExtraFields()
|
|
|
|
: $entry->getCdExtraFields();
|
2019-12-30 18:47:37 +03:00
|
|
|
$extraData = '';
|
|
|
|
|
|
|
|
foreach ($collection as $extraField) {
|
|
|
|
if ($local) {
|
|
|
|
$data = $extraField->packLocalFileData();
|
|
|
|
} else {
|
|
|
|
$data = $extraField->packCentralDirData();
|
|
|
|
}
|
|
|
|
$extraData .= pack(
|
|
|
|
'vv',
|
|
|
|
$extraField->getHeaderId(),
|
|
|
|
\strlen($data)
|
|
|
|
);
|
|
|
|
$extraData .= $data;
|
|
|
|
}
|
|
|
|
|
|
|
|
$size = \strlen($extraData);
|
|
|
|
|
2021-12-12 12:50:30 +03:00
|
|
|
if ($size > 0xFFFF) {
|
2019-12-30 18:47:37 +03:00
|
|
|
throw new ZipException(
|
|
|
|
sprintf(
|
|
|
|
'Size extra out of range: %d. Extra data: %s',
|
|
|
|
$size,
|
|
|
|
$extraData
|
|
|
|
)
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
return $extraData;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param resource $outStream
|
|
|
|
*
|
|
|
|
* @throws ZipException
|
|
|
|
*/
|
2021-02-22 13:12:01 +03:00
|
|
|
protected function writeData($outStream, ZipEntry $entry): void
|
2019-12-30 18:47:37 +03:00
|
|
|
{
|
|
|
|
$zipData = $entry->getData();
|
|
|
|
|
|
|
|
if ($zipData === null) {
|
|
|
|
if ($entry->isDirectory()) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
throw new ZipException(sprintf('No zip data for entry "%s"', $entry->getName()));
|
|
|
|
}
|
|
|
|
|
|
|
|
// data write variants:
|
|
|
|
// --------------------
|
|
|
|
// * data of source zip file -> copy compressed data
|
|
|
|
// * store - simple write
|
|
|
|
// * store and encryption - apply encryption filter and simple write
|
|
|
|
// * deflate or bzip2 - apply compression filter and simple write
|
|
|
|
// * (deflate or bzip2) and encryption - create temp stream and apply
|
|
|
|
// compression filter to it, then apply encryption filter to root
|
|
|
|
// stream and write temp stream data.
|
|
|
|
// (PHP cannot apply the filter for encryption after the compression
|
|
|
|
// filter, so a temporary stream is created for the compressed data)
|
|
|
|
|
2020-01-06 11:53:17 +03:00
|
|
|
if ($zipData instanceof ZipSourceFileData && !$zipData->hasRecompressData($entry)) {
|
2019-12-30 18:47:37 +03:00
|
|
|
// data of source zip file -> copy compressed data
|
|
|
|
$zipData->copyCompressedDataToStream($outStream);
|
|
|
|
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
$entryStream = $zipData->getDataAsStream();
|
|
|
|
|
|
|
|
if (stream_get_meta_data($entryStream)['seekable']) {
|
|
|
|
rewind($entryStream);
|
|
|
|
}
|
|
|
|
|
|
|
|
$uncompressedSize = $entry->getUncompressedSize();
|
|
|
|
|
|
|
|
$posBeforeWrite = ftell($outStream);
|
|
|
|
$compressionMethod = $entry->getCompressionMethod();
|
|
|
|
|
|
|
|
if ($entry->isEncrypted()) {
|
|
|
|
if ($compressionMethod === ZipCompressionMethod::STORED) {
|
|
|
|
$contextFilter = $this->appendEncryptionFilter($outStream, $entry, $uncompressedSize);
|
|
|
|
$checksum = $this->writeAndCountChecksum($entryStream, $outStream, $uncompressedSize);
|
|
|
|
} else {
|
|
|
|
$compressStream = fopen('php://temp', 'w+b');
|
|
|
|
$contextFilter = $this->appendCompressionFilter($compressStream, $entry);
|
|
|
|
$checksum = $this->writeAndCountChecksum($entryStream, $compressStream, $uncompressedSize);
|
|
|
|
|
|
|
|
if ($contextFilter !== null) {
|
|
|
|
stream_filter_remove($contextFilter);
|
|
|
|
$contextFilter = null;
|
|
|
|
}
|
|
|
|
|
|
|
|
rewind($compressStream);
|
|
|
|
|
|
|
|
$compressedSize = fstat($compressStream)['size'];
|
|
|
|
$contextFilter = $this->appendEncryptionFilter($outStream, $entry, $compressedSize);
|
|
|
|
|
|
|
|
stream_copy_to_stream($compressStream, $outStream);
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
$contextFilter = $this->appendCompressionFilter($outStream, $entry);
|
|
|
|
$checksum = $this->writeAndCountChecksum($entryStream, $outStream, $uncompressedSize);
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($contextFilter !== null) {
|
|
|
|
stream_filter_remove($contextFilter);
|
|
|
|
$contextFilter = null;
|
|
|
|
}
|
|
|
|
|
|
|
|
// my hack {@see https://bugs.php.net/bug.php?id=49874}
|
|
|
|
fseek($outStream, 0, \SEEK_END);
|
|
|
|
$compressedSize = ftell($outStream) - $posBeforeWrite;
|
|
|
|
|
|
|
|
$entry->setCompressedSize($compressedSize);
|
|
|
|
$entry->setCrc($checksum);
|
|
|
|
|
|
|
|
if (!$entry->isDataDescriptorEnabled()) {
|
|
|
|
if ($uncompressedSize > ZipConstants::ZIP64_MAGIC || $compressedSize > ZipConstants::ZIP64_MAGIC) {
|
|
|
|
/** @var Zip64ExtraField|null $zip64ExtraLocal */
|
|
|
|
$zip64ExtraLocal = $entry->getLocalExtraField(Zip64ExtraField::HEADER_ID);
|
|
|
|
|
|
|
|
// if there is a zip64 extra record, then update it;
|
|
|
|
// if not, write data to data descriptor
|
|
|
|
if ($zip64ExtraLocal !== null) {
|
|
|
|
$zip64ExtraLocal->setCompressedSize($compressedSize);
|
|
|
|
$zip64ExtraLocal->setUncompressedSize($uncompressedSize);
|
|
|
|
|
|
|
|
$posExtra = $entry->getLocalHeaderOffset() + ZipConstants::LFH_FILENAME_POS + \strlen($entry->getName());
|
|
|
|
fseek($outStream, $posExtra);
|
|
|
|
fwrite($outStream, $this->getExtraFieldsContents($entry, true));
|
|
|
|
} else {
|
|
|
|
$posGPBF = $entry->getLocalHeaderOffset() + 6;
|
|
|
|
$entry->enableDataDescriptor(true);
|
|
|
|
fseek($outStream, $posGPBF);
|
|
|
|
fwrite(
|
|
|
|
$outStream,
|
|
|
|
pack(
|
|
|
|
'v',
|
|
|
|
// general purpose bit flag 2 bytes
|
|
|
|
$entry->getGeneralPurposeBitFlags()
|
|
|
|
)
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
$compressedSize = ZipConstants::ZIP64_MAGIC;
|
|
|
|
$uncompressedSize = ZipConstants::ZIP64_MAGIC;
|
|
|
|
}
|
|
|
|
|
|
|
|
$posChecksum = $entry->getLocalHeaderOffset() + 14;
|
|
|
|
|
|
|
|
/** @var WinZipAesExtraField|null $winZipAesExtra */
|
|
|
|
$winZipAesExtra = $entry->getLocalExtraField(WinZipAesExtraField::HEADER_ID);
|
|
|
|
|
|
|
|
if ($winZipAesExtra !== null && $winZipAesExtra->isV2()) {
|
|
|
|
$checksum = 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
fseek($outStream, $posChecksum);
|
|
|
|
fwrite(
|
|
|
|
$outStream,
|
|
|
|
pack(
|
|
|
|
'VVV',
|
|
|
|
// crc-32 4 bytes
|
|
|
|
$checksum,
|
|
|
|
// compressed size 4 bytes
|
|
|
|
$compressedSize,
|
|
|
|
// uncompressed size 4 bytes
|
|
|
|
$uncompressedSize
|
|
|
|
)
|
|
|
|
);
|
|
|
|
fseek($outStream, 0, \SEEK_END);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param resource $inStream
|
|
|
|
* @param resource $outStream
|
|
|
|
*/
|
2021-02-22 13:12:01 +03:00
|
|
|
private function writeAndCountChecksum($inStream, $outStream, int $size): int
|
2019-12-30 18:47:37 +03:00
|
|
|
{
|
|
|
|
$contextHash = hash_init('crc32b');
|
|
|
|
$offset = 0;
|
|
|
|
|
|
|
|
while ($offset < $size) {
|
|
|
|
$read = min(self::CHUNK_SIZE, $size - $offset);
|
|
|
|
$buffer = fread($inStream, $read);
|
|
|
|
fwrite($outStream, $buffer);
|
|
|
|
hash_update($contextHash, $buffer);
|
|
|
|
$offset += $read;
|
|
|
|
}
|
|
|
|
|
|
|
|
return (int) hexdec(hash_final($contextHash));
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param resource $outStream
|
|
|
|
*
|
|
|
|
* @throws ZipUnsupportMethodException
|
|
|
|
*
|
|
|
|
* @return resource|null
|
|
|
|
*/
|
|
|
|
protected function appendCompressionFilter($outStream, ZipEntry $entry)
|
|
|
|
{
|
|
|
|
$contextCompress = null;
|
|
|
|
switch ($entry->getCompressionMethod()) {
|
|
|
|
case ZipCompressionMethod::DEFLATED:
|
|
|
|
if (!($contextCompress = stream_filter_append(
|
|
|
|
$outStream,
|
|
|
|
'zlib.deflate',
|
|
|
|
\STREAM_FILTER_WRITE,
|
|
|
|
['level' => $entry->getCompressionLevel()]
|
|
|
|
))) {
|
|
|
|
throw new \RuntimeException('Could not append filter "zlib.deflate" to out stream');
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
|
|
|
|
case ZipCompressionMethod::BZIP2:
|
|
|
|
if (!($contextCompress = stream_filter_append(
|
|
|
|
$outStream,
|
|
|
|
'bzip2.compress',
|
|
|
|
\STREAM_FILTER_WRITE,
|
|
|
|
['blocks' => $entry->getCompressionLevel(), 'work' => 0]
|
|
|
|
))) {
|
|
|
|
throw new \RuntimeException('Could not append filter "bzip2.compress" to out stream');
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
|
|
|
|
case ZipCompressionMethod::STORED:
|
|
|
|
// file without compression, do nothing
|
|
|
|
break;
|
|
|
|
|
|
|
|
default:
|
|
|
|
throw new ZipUnsupportMethodException(
|
|
|
|
sprintf(
|
|
|
|
'%s (compression method %d (%s) is not supported)',
|
|
|
|
$entry->getName(),
|
|
|
|
$entry->getCompressionMethod(),
|
|
|
|
ZipCompressionMethod::getCompressionMethodName($entry->getCompressionMethod())
|
|
|
|
)
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
return $contextCompress;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param resource $outStream
|
|
|
|
*
|
|
|
|
* @return resource|null
|
|
|
|
*/
|
2021-02-22 13:12:01 +03:00
|
|
|
protected function appendEncryptionFilter($outStream, ZipEntry $entry, int $size)
|
2019-12-30 18:47:37 +03:00
|
|
|
{
|
|
|
|
$encContextFilter = null;
|
|
|
|
|
|
|
|
if ($entry->isEncrypted()) {
|
|
|
|
if ($entry->getEncryptionMethod() === ZipEncryptionMethod::PKWARE) {
|
|
|
|
PKEncryptionStreamFilter::register();
|
|
|
|
$cipherFilterName = PKEncryptionStreamFilter::FILTER_NAME;
|
|
|
|
} else {
|
|
|
|
WinZipAesEncryptionStreamFilter::register();
|
|
|
|
$cipherFilterName = WinZipAesEncryptionStreamFilter::FILTER_NAME;
|
|
|
|
}
|
|
|
|
$encContextFilter = stream_filter_append(
|
|
|
|
$outStream,
|
|
|
|
$cipherFilterName,
|
|
|
|
\STREAM_FILTER_WRITE,
|
|
|
|
[
|
|
|
|
'entry' => $entry,
|
|
|
|
'size' => $size,
|
|
|
|
]
|
|
|
|
);
|
|
|
|
|
|
|
|
if (!$encContextFilter) {
|
|
|
|
throw new \RuntimeException('Not apply filter ' . $cipherFilterName);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return $encContextFilter;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param resource $outStream
|
|
|
|
*/
|
2021-02-22 13:12:01 +03:00
|
|
|
protected function writeDataDescriptor($outStream, ZipEntry $entry): void
|
2019-12-30 18:47:37 +03:00
|
|
|
{
|
|
|
|
$crc = $entry->getCrc();
|
|
|
|
|
|
|
|
/** @var WinZipAesExtraField|null $winZipAesExtra */
|
|
|
|
$winZipAesExtra = $entry->getLocalExtraField(WinZipAesExtraField::HEADER_ID);
|
|
|
|
|
|
|
|
if ($winZipAesExtra !== null && $winZipAesExtra->isV2()) {
|
|
|
|
$crc = 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
fwrite(
|
|
|
|
$outStream,
|
|
|
|
pack(
|
|
|
|
'VV',
|
|
|
|
// data descriptor signature 4 bytes (0x08074b50)
|
|
|
|
ZipConstants::DATA_DESCRIPTOR,
|
|
|
|
// crc-32 4 bytes
|
|
|
|
$crc
|
|
|
|
)
|
|
|
|
);
|
|
|
|
|
|
|
|
if (
|
2021-02-22 13:12:01 +03:00
|
|
|
$entry->isZip64ExtensionsRequired()
|
|
|
|
|| $entry->getLocalExtraFields()->has(Zip64ExtraField::HEADER_ID)
|
2019-12-30 18:47:37 +03:00
|
|
|
) {
|
2021-02-22 13:12:01 +03:00
|
|
|
$dd = pack(
|
|
|
|
'PP',
|
2019-12-30 18:47:37 +03:00
|
|
|
// compressed size 8 bytes
|
2021-02-22 13:12:01 +03:00
|
|
|
$entry->getCompressedSize(),
|
2019-12-30 18:47:37 +03:00
|
|
|
// uncompressed size 8 bytes
|
2021-02-22 13:12:01 +03:00
|
|
|
$entry->getUncompressedSize()
|
|
|
|
);
|
2019-12-30 18:47:37 +03:00
|
|
|
} else {
|
|
|
|
$dd = pack(
|
|
|
|
'VV',
|
|
|
|
// compressed size 4 bytes
|
|
|
|
$entry->getCompressedSize(),
|
|
|
|
// uncompressed size 4 bytes
|
|
|
|
$entry->getUncompressedSize()
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
fwrite($outStream, $dd);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param resource $outStream
|
|
|
|
*
|
|
|
|
* @throws ZipException
|
|
|
|
*/
|
2021-02-22 13:12:01 +03:00
|
|
|
protected function writeCentralDirectoryBlock($outStream): void
|
2019-12-30 18:47:37 +03:00
|
|
|
{
|
|
|
|
foreach ($this->zipContainer->getEntries() as $outputEntry) {
|
|
|
|
$this->writeCentralDirectoryHeader($outStream, $outputEntry);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Writes a Central File Header record.
|
|
|
|
*
|
|
|
|
* @param resource $outStream
|
|
|
|
*
|
|
|
|
* @throws ZipException
|
|
|
|
*/
|
2021-02-22 13:12:01 +03:00
|
|
|
protected function writeCentralDirectoryHeader($outStream, ZipEntry $entry): void
|
2019-12-30 18:47:37 +03:00
|
|
|
{
|
|
|
|
$compressedSize = $entry->getCompressedSize();
|
|
|
|
$uncompressedSize = $entry->getUncompressedSize();
|
|
|
|
$localHeaderOffset = $entry->getLocalHeaderOffset();
|
|
|
|
|
|
|
|
$entry->getCdExtraFields()->remove(Zip64ExtraField::HEADER_ID);
|
|
|
|
|
|
|
|
if (
|
2021-02-22 13:12:01 +03:00
|
|
|
$localHeaderOffset > ZipConstants::ZIP64_MAGIC
|
|
|
|
|| $compressedSize > ZipConstants::ZIP64_MAGIC
|
|
|
|
|| $uncompressedSize > ZipConstants::ZIP64_MAGIC
|
2019-12-30 18:47:37 +03:00
|
|
|
) {
|
|
|
|
$zip64ExtraField = new Zip64ExtraField();
|
|
|
|
|
|
|
|
if ($uncompressedSize >= ZipConstants::ZIP64_MAGIC) {
|
|
|
|
$zip64ExtraField->setUncompressedSize($uncompressedSize);
|
|
|
|
$uncompressedSize = ZipConstants::ZIP64_MAGIC;
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($compressedSize >= ZipConstants::ZIP64_MAGIC) {
|
|
|
|
$zip64ExtraField->setCompressedSize($compressedSize);
|
|
|
|
$compressedSize = ZipConstants::ZIP64_MAGIC;
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($localHeaderOffset >= ZipConstants::ZIP64_MAGIC) {
|
|
|
|
$zip64ExtraField->setLocalHeaderOffset($localHeaderOffset);
|
|
|
|
$localHeaderOffset = ZipConstants::ZIP64_MAGIC;
|
|
|
|
}
|
|
|
|
|
|
|
|
$entry->getCdExtraFields()->add($zip64ExtraField);
|
|
|
|
}
|
|
|
|
|
|
|
|
$extra = $this->getExtraFieldsContents($entry, false);
|
|
|
|
$extraLength = \strlen($extra);
|
|
|
|
|
|
|
|
$name = $entry->getName();
|
|
|
|
$comment = $entry->getComment();
|
|
|
|
|
|
|
|
$dosCharset = $entry->getCharset();
|
|
|
|
|
|
|
|
if ($dosCharset !== null && !$entry->isUtf8Flag()) {
|
|
|
|
$name = DosCodePage::fromUTF8($name, $dosCharset);
|
|
|
|
|
|
|
|
if ($comment) {
|
|
|
|
$comment = DosCodePage::fromUTF8($comment, $dosCharset);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
$commentLength = \strlen($comment);
|
|
|
|
|
|
|
|
$compressionMethod = $entry->getCompressionMethod();
|
|
|
|
$crc = $entry->getCrc();
|
|
|
|
|
|
|
|
/** @var WinZipAesExtraField|null $winZipAesExtra */
|
|
|
|
$winZipAesExtra = $entry->getLocalExtraField(WinZipAesExtraField::HEADER_ID);
|
|
|
|
|
|
|
|
if ($winZipAesExtra !== null) {
|
|
|
|
if ($winZipAesExtra->isV2()) {
|
|
|
|
$crc = 0;
|
|
|
|
}
|
|
|
|
$compressionMethod = ZipCompressionMethod::WINZIP_AES;
|
|
|
|
}
|
|
|
|
|
|
|
|
fwrite(
|
|
|
|
$outStream,
|
|
|
|
pack(
|
|
|
|
'VvvvvVVVVvvvvvVV',
|
|
|
|
// central file header signature 4 bytes (0x02014b50)
|
|
|
|
ZipConstants::CENTRAL_FILE_HEADER,
|
|
|
|
// version made by 2 bytes
|
|
|
|
($entry->getCreatedOS() << 8) | $entry->getSoftwareVersion(),
|
|
|
|
// version needed to extract 2 bytes
|
|
|
|
($entry->getExtractedOS() << 8) | $entry->getExtractVersion(),
|
|
|
|
// general purpose bit flag 2 bytes
|
|
|
|
$entry->getGeneralPurposeBitFlags(),
|
|
|
|
// compression method 2 bytes
|
|
|
|
$compressionMethod,
|
|
|
|
// last mod file datetime 4 bytes
|
|
|
|
$entry->getDosTime(),
|
|
|
|
// crc-32 4 bytes
|
|
|
|
$crc,
|
|
|
|
// compressed size 4 bytes
|
|
|
|
$compressedSize,
|
|
|
|
// uncompressed size 4 bytes
|
|
|
|
$uncompressedSize,
|
|
|
|
// file name length 2 bytes
|
|
|
|
\strlen($name),
|
|
|
|
// extra field length 2 bytes
|
|
|
|
$extraLength,
|
|
|
|
// 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
|
|
|
|
$localHeaderOffset
|
|
|
|
)
|
|
|
|
);
|
|
|
|
|
|
|
|
// file name (variable size)
|
|
|
|
fwrite($outStream, $name);
|
|
|
|
|
|
|
|
if ($extraLength > 0) {
|
|
|
|
// extra field (variable size)
|
|
|
|
fwrite($outStream, $extra);
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($commentLength > 0) {
|
|
|
|
// file comment (variable size)
|
|
|
|
fwrite($outStream, $comment);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param resource $outStream
|
|
|
|
*/
|
|
|
|
protected function writeEndOfCentralDirectoryBlock(
|
|
|
|
$outStream,
|
2021-02-22 13:12:01 +03:00
|
|
|
int $centralDirectoryOffset,
|
|
|
|
int $centralDirectorySize
|
|
|
|
): void {
|
2019-12-30 18:47:37 +03:00
|
|
|
$cdEntriesCount = \count($this->zipContainer);
|
|
|
|
|
2021-12-12 12:50:30 +03:00
|
|
|
$cdEntriesZip64 = $cdEntriesCount > 0xFFFF;
|
2019-12-30 18:47:37 +03:00
|
|
|
$cdSizeZip64 = $centralDirectorySize > ZipConstants::ZIP64_MAGIC;
|
|
|
|
$cdOffsetZip64 = $centralDirectoryOffset > ZipConstants::ZIP64_MAGIC;
|
|
|
|
|
|
|
|
$zip64Required = $cdEntriesZip64
|
|
|
|
|| $cdSizeZip64
|
|
|
|
|| $cdOffsetZip64;
|
|
|
|
|
|
|
|
if ($zip64Required) {
|
|
|
|
$zip64EndOfCentralDirectoryOffset = ftell($outStream);
|
|
|
|
|
|
|
|
// find max software version, version needed to extract and most common platform
|
2021-02-22 13:12:01 +03:00
|
|
|
[$softwareVersion, $versionNeededToExtract] = array_reduce(
|
2019-12-30 18:47:37 +03:00
|
|
|
$this->zipContainer->getEntries(),
|
|
|
|
static function (array $carry, ZipEntry $entry) {
|
|
|
|
$carry[0] = max($carry[0], $entry->getSoftwareVersion() & 0xFF);
|
|
|
|
$carry[1] = max($carry[1], $entry->getExtractVersion() & 0xFF);
|
|
|
|
|
|
|
|
return $carry;
|
|
|
|
},
|
|
|
|
[ZipVersion::v10_DEFAULT_MIN, ZipVersion::v45_ZIP64_EXT]
|
|
|
|
);
|
|
|
|
|
|
|
|
$createdOS = $extractedOS = ZipPlatform::OS_DOS;
|
|
|
|
$versionMadeBy = ($createdOS << 8) | max($softwareVersion, ZipVersion::v45_ZIP64_EXT);
|
|
|
|
$versionExtractedBy = ($extractedOS << 8) | max($versionNeededToExtract, ZipVersion::v45_ZIP64_EXT);
|
|
|
|
|
|
|
|
// write zip64 end of central directory signature
|
|
|
|
fwrite(
|
|
|
|
$outStream,
|
|
|
|
pack(
|
2021-02-22 13:12:01 +03:00
|
|
|
'VPvvVVPPPPVVPV',
|
2019-12-30 18:47:37 +03:00
|
|
|
// signature 4 bytes (0x06064b50)
|
2021-02-22 13:12:01 +03:00
|
|
|
ZipConstants::ZIP64_END_CD,
|
|
|
|
// size of zip64 end of central
|
|
|
|
// directory record 8 bytes
|
|
|
|
ZipConstants::ZIP64_END_OF_CD_LEN - 12,
|
2019-12-30 18:47:37 +03:00
|
|
|
// 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
|
2021-02-22 13:12:01 +03:00
|
|
|
0,
|
|
|
|
// total number of entries in the
|
|
|
|
// central directory on this disk 8 bytes
|
|
|
|
$cdEntriesCount,
|
|
|
|
// total number of entries in the
|
|
|
|
// central directory 8 bytes
|
|
|
|
$cdEntriesCount,
|
|
|
|
// size of the central directory 8 bytes
|
|
|
|
$centralDirectorySize,
|
|
|
|
// offset of start of central
|
|
|
|
// directory with respect to
|
|
|
|
// the starting disk number 8 bytes
|
|
|
|
$centralDirectoryOffset,
|
2019-12-30 18:47:37 +03:00
|
|
|
// zip64 end of central dir locator
|
|
|
|
// signature 4 bytes (0x07064b50)
|
|
|
|
ZipConstants::ZIP64_END_CD_LOC,
|
|
|
|
// number of the disk with the
|
|
|
|
// start of the zip64 end of
|
|
|
|
// central directory 4 bytes
|
2021-02-22 13:12:01 +03:00
|
|
|
0,
|
|
|
|
// relative offset of the zip64
|
|
|
|
// end of central directory record 8 bytes
|
|
|
|
$zip64EndOfCentralDirectoryOffset,
|
|
|
|
// total number of disks 4 bytes
|
|
|
|
1
|
|
|
|
)
|
2019-12-30 18:47:37 +03:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
$comment = $this->zipContainer->getArchiveComment();
|
|
|
|
$commentLength = $comment !== null ? \strlen($comment) : 0;
|
|
|
|
|
|
|
|
fwrite(
|
|
|
|
$outStream,
|
|
|
|
pack(
|
|
|
|
'VvvvvVVv',
|
|
|
|
// end of central dir signature 4 bytes (0x06054b50)
|
|
|
|
ZipConstants::END_CD,
|
|
|
|
// 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
|
2021-12-12 12:50:30 +03:00
|
|
|
$cdEntriesZip64 ? 0xFFFF : $cdEntriesCount,
|
2019-12-30 18:47:37 +03:00
|
|
|
// total number of entries in
|
|
|
|
// the central directory 2 bytes
|
2021-12-12 12:50:30 +03:00
|
|
|
$cdEntriesZip64 ? 0xFFFF : $cdEntriesCount,
|
2019-12-30 18:47:37 +03:00
|
|
|
// size of the central directory 4 bytes
|
|
|
|
$cdSizeZip64 ? ZipConstants::ZIP64_MAGIC : $centralDirectorySize,
|
|
|
|
// offset of start of central
|
|
|
|
// directory with respect to
|
|
|
|
// the starting disk number 4 bytes
|
|
|
|
$cdOffsetZip64 ? ZipConstants::ZIP64_MAGIC : $centralDirectoryOffset,
|
|
|
|
// .ZIP file comment length 2 bytes
|
|
|
|
$commentLength
|
|
|
|
)
|
|
|
|
);
|
|
|
|
|
|
|
|
if ($comment !== null && $commentLength > 0) {
|
|
|
|
// .ZIP file comment (variable size)
|
|
|
|
fwrite($outStream, $comment);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|