529 lines
21 KiB
PHP
529 lines
21 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\ZipFileInterface;
|
|
|
|
/**
|
|
* Write
|
|
* ip 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
|
|
* @throws InvalidArgumentException
|
|
*/
|
|
public function __construct($out, ZipModel $zipModel)
|
|
{
|
|
if (!is_resource($out)) {
|
|
throw new InvalidArgumentException('$out must be resource');
|
|
}
|
|
$this->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;
|
|
}
|
|
}
|