diff options
Diffstat (limited to 'vendor/nelexa/zip/src/PhpZip/Stream/ZipInputStream.php')
-rw-r--r-- | vendor/nelexa/zip/src/PhpZip/Stream/ZipInputStream.php | 603 |
1 files changed, 603 insertions, 0 deletions
diff --git a/vendor/nelexa/zip/src/PhpZip/Stream/ZipInputStream.php b/vendor/nelexa/zip/src/PhpZip/Stream/ZipInputStream.php new file mode 100644 index 0000000..8c8adf5 --- /dev/null +++ b/vendor/nelexa/zip/src/PhpZip/Stream/ZipInputStream.php @@ -0,0 +1,603 @@ +<?php + +namespace PhpZip\Stream; + +use PhpZip\Crypto\TraditionalPkwareEncryptionEngine; +use PhpZip\Crypto\WinZipAesEngine; +use PhpZip\Exception\Crc32Exception; +use PhpZip\Exception\InvalidArgumentException; +use PhpZip\Exception\RuntimeException; +use PhpZip\Exception\ZipCryptoException; +use PhpZip\Exception\ZipException; +use PhpZip\Exception\ZipUnsupportMethod; +use PhpZip\Extra\ExtraFieldsCollection; +use PhpZip\Extra\ExtraFieldsFactory; +use PhpZip\Extra\Fields\ApkAlignmentExtraField; +use PhpZip\Extra\Fields\WinZipAesEntryExtraField; +use PhpZip\Mapper\OffsetPositionMapper; +use PhpZip\Mapper\PositionMapper; +use PhpZip\Model\EndOfCentralDirectory; +use PhpZip\Model\Entry\ZipSourceEntry; +use PhpZip\Model\ZipEntry; +use PhpZip\Model\ZipModel; +use PhpZip\Util\PackUtil; +use PhpZip\Util\StringUtil; +use PhpZip\ZipFileInterface; + +/** + * Read zip file + * + * @author Ne-Lexa alexey@nelexa.ru + * @license MIT + */ +class ZipInputStream implements ZipInputStreamInterface +{ + /** + * @var resource + */ + protected $in; + /** + * @var PositionMapper + */ + protected $mapper; + /** + * @var int The number of bytes in the preamble of this ZIP file. + */ + protected $preamble = 0; + /** + * @var int The number of bytes in the postamble of this ZIP file. + */ + protected $postamble = 0; + /** + * @var ZipModel + */ + protected $zipModel; + + /** + * ZipInputStream constructor. + * @param resource $in + * @throws RuntimeException + */ + public function __construct($in) + { + if (!is_resource($in)) { + throw new RuntimeException('$in must be resource'); + } + $this->in = $in; + $this->mapper = new PositionMapper(); + } + + /** + * @return ZipModel + */ + public function readZip() + { + $this->checkZipFileSignature(); + $endOfCentralDirectory = $this->readEndOfCentralDirectory(); + $entries = $this->mountCentralDirectory($endOfCentralDirectory); + $this->zipModel = ZipModel::newSourceModel($entries, $endOfCentralDirectory); + return $this->zipModel; + } + + /** + * Check zip file signature + * + * @throws ZipException if this not .ZIP file. + */ + protected function checkZipFileSignature() + { + rewind($this->in); + // Constraint: A ZIP file must start with a Local File Header + // or a (ZIP64) End Of Central Directory Record if it's empty. + $signatureBytes = fread($this->in, 4); + if (strlen($signatureBytes) < 4) { + throw new ZipException("Invalid zip file."); + } + $signature = unpack('V', $signatureBytes)[1]; + if ( + ZipEntry::LOCAL_FILE_HEADER_SIG !== $signature + && EndOfCentralDirectory::ZIP64_END_OF_CENTRAL_DIRECTORY_RECORD_SIG !== $signature + && EndOfCentralDirectory::END_OF_CENTRAL_DIRECTORY_RECORD_SIG !== $signature + ) { + throw new ZipException("Expected Local File Header or (ZIP64) End Of Central Directory Record! Signature: " . $signature); + } + } + + /** + * @return EndOfCentralDirectory + * @throws ZipException + */ + protected function readEndOfCentralDirectory() + { + $comment = null; + // Search for End of central directory record. + $stats = fstat($this->in); + $size = $stats['size']; + $max = $size - EndOfCentralDirectory::END_OF_CENTRAL_DIRECTORY_RECORD_MIN_LEN; + $min = $max >= 0xffff ? $max - 0xffff : 0; + for ($endOfCentralDirRecordPos = $max; $endOfCentralDirRecordPos >= $min; $endOfCentralDirRecordPos--) { + fseek($this->in, $endOfCentralDirRecordPos, SEEK_SET); + // end of central dir signature 4 bytes (0x06054b50) + if (EndOfCentralDirectory::END_OF_CENTRAL_DIRECTORY_RECORD_SIG !== unpack('V', fread($this->in, 4))[1]) { + continue; + } + + // number of this disk - 2 bytes + // number of the disk with the start of the + // central directory - 2 bytes + // total number of entries in the central + // directory on this disk - 2 bytes + // total number of entries in the central + // directory - 2 bytes + // size of the central directory - 4 bytes + // offset of start of central directory with + // respect to the starting disk number - 4 bytes + // ZIP file comment length - 2 bytes + $data = unpack( + 'vdiskNo/vcdDiskNo/vcdEntriesDisk/vcdEntries/VcdSize/VcdPos/vcommentLength', + fread($this->in, 18) + ); + + if (0 !== $data['diskNo'] || 0 !== $data['cdDiskNo'] || $data['cdEntriesDisk'] !== $data['cdEntries']) { + throw new ZipException( + "ZIP file spanning/splitting is not supported!" + ); + } + // .ZIP file comment (variable size) + if (0 < $data['commentLength']) { + $comment = fread($this->in, $data['commentLength']); + } + $this->preamble = $endOfCentralDirRecordPos; + $this->postamble = $size - ftell($this->in); + + // Check for ZIP64 End Of Central Directory Locator. + $endOfCentralDirLocatorPos = $endOfCentralDirRecordPos - EndOfCentralDirectory::ZIP64_END_OF_CENTRAL_DIRECTORY_LOCATOR_LEN; + + fseek($this->in, $endOfCentralDirLocatorPos, SEEK_SET); + // zip64 end of central dir locator + // signature 4 bytes (0x07064b50) + if ( + 0 > $endOfCentralDirLocatorPos || + ftell($this->in) === $size || + EndOfCentralDirectory::ZIP64_END_OF_CENTRAL_DIRECTORY_LOCATOR_SIG !== unpack('V', fread($this->in, 4))[1] + ) { + // Seek and check first CFH, probably requiring an offset mapper. + $offset = $endOfCentralDirRecordPos - $data['cdSize']; + fseek($this->in, $offset, SEEK_SET); + $offset -= $data['cdPos']; + if (0 !== $offset) { + $this->mapper = new OffsetPositionMapper($offset); + } + $entryCount = $data['cdEntries']; + return new EndOfCentralDirectory($entryCount, $comment); + } + + // number of the disk with the + // start of the zip64 end of + // central directory 4 bytes + $zip64EndOfCentralDirectoryRecordDisk = unpack('V', fread($this->in, 4))[1]; + // relative offset of the zip64 + // end of central directory record 8 bytes + $zip64EndOfCentralDirectoryRecordPos = PackUtil::unpackLongLE(fread($this->in, 8)); + // total number of disks 4 bytes + $totalDisks = unpack('V', fread($this->in, 4))[1]; + if (0 !== $zip64EndOfCentralDirectoryRecordDisk || 1 !== $totalDisks) { + throw new ZipException("ZIP file spanning/splitting is not supported!"); + } + fseek($this->in, $zip64EndOfCentralDirectoryRecordPos, SEEK_SET); + // zip64 end of central dir + // signature 4 bytes (0x06064b50) + $zip64EndOfCentralDirSig = unpack('V', fread($this->in, 4))[1]; + if (EndOfCentralDirectory::ZIP64_END_OF_CENTRAL_DIRECTORY_RECORD_SIG !== $zip64EndOfCentralDirSig) { + throw new ZipException("Expected ZIP64 End Of Central Directory Record!"); + } + // size of zip64 end of central + // directory record 8 bytes + // version made by 2 bytes + // version needed to extract 2 bytes + fseek($this->in, 12, SEEK_CUR); + // number of this disk 4 bytes + $diskNo = unpack('V', fread($this->in, 4))[1]; + // number of the disk with the + // start of the central directory 4 bytes + $cdDiskNo = unpack('V', fread($this->in, 4))[1]; + // total number of entries in the + // central directory on this disk 8 bytes + $cdEntriesDisk = PackUtil::unpackLongLE(fread($this->in, 8)); + // total number of entries in the + // central directory 8 bytes + $cdEntries = PackUtil::unpackLongLE(fread($this->in, 8)); + if (0 !== $diskNo || 0 !== $cdDiskNo || $cdEntriesDisk !== $cdEntries) { + throw new ZipException("ZIP file spanning/splitting is not supported!"); + } + if ($cdEntries < 0 || 0x7fffffff < $cdEntries) { + throw new ZipException("Total Number Of Entries In The Central Directory out of range!"); + } + // size of the central directory 8 bytes + fseek($this->in, 8, SEEK_CUR); + // offset of start of central + // directory with respect to + // the starting disk number 8 bytes + $cdPos = PackUtil::unpackLongLE(fread($this->in, 8)); + // zip64 extensible data sector (variable size) + fseek($this->in, $cdPos, SEEK_SET); + $this->preamble = $zip64EndOfCentralDirectoryRecordPos; + $entryCount = $cdEntries; + $zip64 = true; + return new EndOfCentralDirectory($entryCount, $comment, $zip64); + } + // Start recovering file entries from min. + $this->preamble = $min; + $this->postamble = $size - $min; + return new EndOfCentralDirectory(0, $comment); + } + + /** + * Reads the central directory from the given seekable byte channel + * and populates the internal tables with ZipEntry instances. + * + * The ZipEntry's will know all data that can be obtained from the + * central directory alone, but not the data that requires the local + * file header or additional data to be read. + * + * @param EndOfCentralDirectory $endOfCentralDirectory + * @return ZipEntry[] + * @throws ZipException + */ + protected function mountCentralDirectory(EndOfCentralDirectory $endOfCentralDirectory) + { + $numEntries = $endOfCentralDirectory->getEntryCount(); + $entries = []; + + for (; $numEntries > 0; $numEntries--) { + $entry = $this->readEntry(); + // Re-load virtual offset after ZIP64 Extended Information + // Extra Field may have been parsed, map it to the real + // offset and conditionally update the preamble size from it. + $lfhOff = $this->mapper->map($entry->getOffset()); + $lfhOff = PHP_INT_SIZE === 4 ? sprintf('%u', $lfhOff) : $lfhOff; + if ($lfhOff < $this->preamble) { + $this->preamble = $lfhOff; + } + $entries[$entry->getName()] = $entry; + } + + if (0 !== $numEntries % 0x10000) { + throw new ZipException("Expected " . abs($numEntries) . + ($numEntries > 0 ? " more" : " less") . + " entries in the Central Directory!"); + } + + if ($this->preamble + $this->postamble >= fstat($this->in)['size']) { + assert(0 === $numEntries); + $this->checkZipFileSignature(); + } + + return $entries; + } + + /** + * @return ZipEntry + * @throws InvalidArgumentException + */ + public function readEntry() + { + // central file header signature 4 bytes (0x02014b50) + $fileHeaderSig = unpack('V', fread($this->in, 4))[1]; + if (ZipOutputStreamInterface::CENTRAL_FILE_HEADER_SIG !== $fileHeaderSig) { + throw new InvalidArgumentException("Corrupt zip file. Can not read zip entry."); + } + + // version made by 2 bytes + // version needed to extract 2 bytes + // general purpose bit flag 2 bytes + // compression method 2 bytes + // last mod file time 2 bytes + // last mod file date 2 bytes + // crc-32 4 bytes + // compressed size 4 bytes + // uncompressed size 4 bytes + // file name length 2 bytes + // extra field length 2 bytes + // file comment length 2 bytes + // disk number start 2 bytes + // internal file attributes 2 bytes + // external file attributes 4 bytes + // relative offset of local header 4 bytes + $data = unpack( + 'vversionMadeBy/vversionNeededToExtract/vgpbf/' . + 'vrawMethod/VrawTime/VrawCrc/VrawCompressedSize/' . + 'VrawSize/vfileLength/vextraLength/vcommentLength/' . + 'VrawInternalAttributes/VrawExternalAttributes/VlfhOff', + fread($this->in, 42) + ); + +// $utf8 = 0 !== ($data['gpbf'] & self::GPBF_UTF8); + + // See appendix D of PKWARE's ZIP File Format Specification. + $name = fread($this->in, $data['fileLength']); + + $entry = new ZipSourceEntry($this); + $entry->setName($name); + $entry->setVersionNeededToExtract($data['versionNeededToExtract']); + $entry->setPlatform($data['versionMadeBy'] >> 8); + $entry->setMethod($data['rawMethod']); + $entry->setGeneralPurposeBitFlags($data['gpbf']); + $entry->setDosTime($data['rawTime']); + $entry->setCrc($data['rawCrc']); + $entry->setCompressedSize($data['rawCompressedSize']); + $entry->setSize($data['rawSize']); + $entry->setExternalAttributes($data['rawExternalAttributes']); + $entry->setOffset($data['lfhOff']); // must be unmapped! + if (0 < $data['extraLength']) { + $entry->setExtra(fread($this->in, $data['extraLength'])); + } + if (0 < $data['commentLength']) { + $entry->setComment(fread($this->in, $data['commentLength'])); + } + return $entry; + } + + /** + * @param ZipEntry $entry + * @return string + * @throws ZipException + */ + public function readEntryContent(ZipEntry $entry) + { + if ($entry->isDirectory()) { + return null; + } + if (!($entry instanceof ZipSourceEntry)) { + throw new InvalidArgumentException('entry must be ' . ZipSourceEntry::class); + } + $isEncrypted = $entry->isEncrypted(); + if ($isEncrypted && null === $entry->getPassword()) { + throw new ZipException("Can not password from entry " . $entry->getName()); + } + + $pos = $entry->getOffset(); + assert(ZipEntry::UNKNOWN !== $pos); + $pos = PHP_INT_SIZE === 4 ? sprintf('%u', $pos) : $pos; + + $startPos = $pos = $this->mapper->map($pos); + fseek($this->in, $startPos); + + // local file header signature 4 bytes (0x04034b50) + if (ZipEntry::LOCAL_FILE_HEADER_SIG !== unpack('V', fread($this->in, 4))[1]) { + throw new ZipException($entry->getName() . " (expected Local File Header)"); + } + fseek($this->in, $pos + ZipEntry::LOCAL_FILE_HEADER_FILE_NAME_LENGTH_POS); + // file name length 2 bytes + // extra field length 2 bytes + $data = unpack('vfileLength/vextraLength', fread($this->in, 4)); + $pos += ZipEntry::LOCAL_FILE_HEADER_MIN_LEN + $data['fileLength'] + $data['extraLength']; + + assert(ZipEntry::UNKNOWN !== $entry->getCrc()); + + $method = $entry->getMethod(); + + fseek($this->in, $pos); + + // Get raw entry content + $compressedSize = $entry->getCompressedSize(); + $compressedSize = PHP_INT_SIZE === 4 ? sprintf('%u', $compressedSize) : $compressedSize; + if ($compressedSize > 0) { + $content = fread($this->in, $compressedSize); + } else { + $content = ''; + } + + $skipCheckCrc = false; + if ($isEncrypted) { + if (ZipEntry::METHOD_WINZIP_AES === $method) { + // Strong Encryption Specification - WinZip AES + $winZipAesEngine = new WinZipAesEngine($entry); + $content = $winZipAesEngine->decrypt($content); + /** + * @var WinZipAesEntryExtraField $field + */ + $field = $entry->getExtraFieldsCollection()->get(WinZipAesEntryExtraField::getHeaderId()); + $method = $field->getMethod(); + $entry->setEncryptionMethod($field->getEncryptionMethod()); + $skipCheckCrc = true; + } else { + // Traditional PKWARE Decryption + $zipCryptoEngine = new TraditionalPkwareEncryptionEngine($entry); + $content = $zipCryptoEngine->decrypt($content); + $entry->setEncryptionMethod(ZipFileInterface::ENCRYPTION_METHOD_TRADITIONAL); + } + + if (!$skipCheckCrc) { + // Check CRC32 in the Local File Header or Data Descriptor. + $localCrc = null; + if ($entry->getGeneralPurposeBitFlag(ZipEntry::GPBF_DATA_DESCRIPTOR)) { + // The CRC32 is in the Data Descriptor after the compressed size. + // Note the Data Descriptor's Signature is optional: + // All newer apps should write it (and so does TrueVFS), + // but older apps might not. + fseek($this->in, $pos + $compressedSize); + $localCrc = unpack('V', fread($this->in, 4))[1]; + if (ZipEntry::DATA_DESCRIPTOR_SIG === $localCrc) { + $localCrc = unpack('V', fread($this->in, 4))[1]; + } + } else { + fseek($this->in, $startPos + 14); + // The CRC32 in the Local File Header. + $localCrc = sprintf('%u', fread($this->in, 4)[1]); + $localCrc = PHP_INT_SIZE === 4 ? sprintf('%u', $localCrc) : $localCrc; + } + + $crc = PHP_INT_SIZE === 4 ? sprintf('%u', $entry->getCrc()) : $entry->getCrc(); + + if ($crc != $localCrc) { + throw new Crc32Exception($entry->getName(), $crc, $localCrc); + } + } + } + + switch ($method) { + case ZipFileInterface::METHOD_STORED: + break; + case ZipFileInterface::METHOD_DEFLATED: + $content = gzinflate($content); + break; + case ZipFileInterface::METHOD_BZIP2: + if (!extension_loaded('bz2')) { + throw new ZipException('Extension bzip2 not install'); + } + $content = bzdecompress($content); + break; + default: + throw new ZipUnsupportMethod($entry->getName() . + " (compression method " . $method . " is not supported)"); + } + if (!$skipCheckCrc AND false) { + $localCrc = crc32($content); + $localCrc = PHP_INT_SIZE === 4 ? sprintf('%u', $localCrc) : $localCrc; + $crc = PHP_INT_SIZE === 4 ? sprintf('%u', $entry->getCrc()) : $entry->getCrc(); + if ($crc != $localCrc) { + if ($isEncrypted) { + throw new ZipCryptoException("Wrong password"); + } + throw new Crc32Exception($entry->getName(), $crc, $localCrc); + } + } + return $content; + } + + /** + * @return resource + */ + public function getStream() + { + return $this->in; + } + + /** + * Copy the input stream of the LOC entry zip and the data into + * the output stream and zip the alignment if necessary. + * + * @param ZipEntry $entry + * @param ZipOutputStreamInterface $out + */ + public function copyEntry(ZipEntry $entry, ZipOutputStreamInterface $out) + { + $pos = $entry->getOffset(); + assert(ZipEntry::UNKNOWN !== $pos); + $pos = PHP_INT_SIZE === 4 ? sprintf('%u', $pos) : $pos; + $pos = $this->mapper->map($pos); + + $nameLength = strlen($entry->getName()); + + fseek($this->in, $pos + ZipEntry::LOCAL_FILE_HEADER_MIN_LEN - 2, SEEK_SET); + $sourceExtraLength = $destExtraLength = unpack('v', fread($this->in, 2))[1]; + + if ($sourceExtraLength > 0) { + // read Local File Header extra fields + fseek($this->in, $pos + ZipEntry::LOCAL_FILE_HEADER_MIN_LEN + $nameLength, SEEK_SET); + $extra = fread($this->in, $sourceExtraLength); + $extraFieldsCollection = ExtraFieldsFactory::createExtraFieldCollections($extra, $entry); + if (isset($extraFieldsCollection[ApkAlignmentExtraField::getHeaderId()]) && $this->zipModel->isZipAlign()) { + unset($extraFieldsCollection[ApkAlignmentExtraField::getHeaderId()]); + $destExtraLength = strlen(ExtraFieldsFactory::createSerializedData($extraFieldsCollection)); + } + } else { + $extraFieldsCollection = new ExtraFieldsCollection(); + } + + $dataAlignmentMultiple = $this->zipModel->getZipAlign(); + $copyInToOutLength = $entry->getCompressedSize(); + + fseek($this->in, $pos, SEEK_SET); + + if ( + $this->zipModel->isZipAlign() && + !$entry->isEncrypted() && + $entry->getMethod() === ZipFileInterface::METHOD_STORED + ) { + if (StringUtil::endsWith($entry->getName(), '.so')) { + $dataAlignmentMultiple = ApkAlignmentExtraField::ANDROID_COMMON_PAGE_ALIGNMENT_BYTES; + } + + $dataMinStartOffset = + ftell($out->getStream()) + + ZipEntry::LOCAL_FILE_HEADER_MIN_LEN + + $destExtraLength + + $nameLength + + ApkAlignmentExtraField::ALIGNMENT_ZIP_EXTRA_MIN_SIZE_BYTES; + $padding = + ($dataAlignmentMultiple - ($dataMinStartOffset % $dataAlignmentMultiple)) + % $dataAlignmentMultiple; + + $alignExtra = new ApkAlignmentExtraField(); + $alignExtra->setMultiple($dataAlignmentMultiple); + $alignExtra->setPadding($padding); + $extraFieldsCollection->add($alignExtra); + + $extra = ExtraFieldsFactory::createSerializedData($extraFieldsCollection); + + // copy Local File Header without extra field length + // from input stream to output stream + stream_copy_to_stream($this->in, $out->getStream(), ZipEntry::LOCAL_FILE_HEADER_MIN_LEN - 2); + // write new extra field length (2 bytes) to output stream + fwrite($out->getStream(), pack('v', strlen($extra))); + // skip 2 bytes to input stream + fseek($this->in, 2, SEEK_CUR); + // copy name from input stream to output stream + stream_copy_to_stream($this->in, $out->getStream(), $nameLength); + // write extra field to output stream + fwrite($out->getStream(), $extra); + // skip source extraLength from input stream + fseek($this->in, $sourceExtraLength, SEEK_CUR); + } else { + $copyInToOutLength += ZipEntry::LOCAL_FILE_HEADER_MIN_LEN + $sourceExtraLength + $nameLength; + ; + } + if ($entry->getGeneralPurposeBitFlag(ZipEntry::GPBF_DATA_DESCRIPTOR)) { +// crc-32 4 bytes +// compressed size 4 bytes +// uncompressed size 4 bytes + $copyInToOutLength += 12; + if ($entry->isZip64ExtensionsRequired()) { +// compressed size +4 bytes +// uncompressed size +4 bytes + $copyInToOutLength += 8; + } + } + // copy loc, data, data descriptor from input to output stream + stream_copy_to_stream($this->in, $out->getStream(), $copyInToOutLength); + } + + /** + * @param ZipEntry $entry + * @param ZipOutputStreamInterface $out + */ + public function copyEntryData(ZipEntry $entry, ZipOutputStreamInterface $out) + { + $offset = $entry->getOffset(); + $offset = PHP_INT_SIZE === 4 ? sprintf('%u', $offset) : $offset; + $offset = $this->mapper->map($offset); + $nameLength = strlen($entry->getName()); + + fseek($this->in, $offset + ZipEntry::LOCAL_FILE_HEADER_MIN_LEN - 2, SEEK_SET); + $extraLength = unpack('v', fread($this->in, 2))[1]; + + fseek($this->in, $offset + ZipEntry::LOCAL_FILE_HEADER_MIN_LEN + $nameLength + $extraLength, SEEK_SET); + // copy raw data from input stream to output stream + stream_copy_to_stream($this->in, $out->getStream(), $entry->getCompressedSize()); + } + + public function __destruct() + { + $this->close(); + } + + public function close() + { + if ($this->in != null) { + fclose($this->in); + $this->in = null; + } + } +} |