Ne-Lexa /
php-zip
| 1 | <?php |
||||
| 2 | |||||
| 3 | namespace PhpZip\IO; |
||||
| 4 | |||||
| 5 | use PhpZip\Constants\DosCodePage; |
||||
| 6 | use PhpZip\Constants\GeneralPurposeBitFlag; |
||||
| 7 | use PhpZip\Constants\ZipCompressionMethod; |
||||
| 8 | use PhpZip\Constants\ZipConstants; |
||||
| 9 | use PhpZip\Constants\ZipEncryptionMethod; |
||||
| 10 | use PhpZip\Constants\ZipOptions; |
||||
| 11 | use PhpZip\Exception\Crc32Exception; |
||||
| 12 | use PhpZip\Exception\InvalidArgumentException; |
||||
| 13 | use PhpZip\Exception\ZipException; |
||||
| 14 | use PhpZip\IO\Filter\Cipher\Pkware\PKDecryptionStreamFilter; |
||||
| 15 | use PhpZip\IO\Filter\Cipher\WinZipAes\WinZipAesDecryptionStreamFilter; |
||||
| 16 | use PhpZip\Model\Data\ZipSourceFileData; |
||||
| 17 | use PhpZip\Model\EndOfCentralDirectory; |
||||
| 18 | use PhpZip\Model\Extra\ExtraFieldsCollection; |
||||
| 19 | use PhpZip\Model\Extra\Fields\UnicodePathExtraField; |
||||
| 20 | use PhpZip\Model\Extra\Fields\UnrecognizedExtraField; |
||||
| 21 | use PhpZip\Model\Extra\Fields\WinZipAesExtraField; |
||||
| 22 | use PhpZip\Model\Extra\Fields\Zip64ExtraField; |
||||
| 23 | use PhpZip\Model\Extra\ZipExtraDriver; |
||||
| 24 | use PhpZip\Model\Extra\ZipExtraField; |
||||
| 25 | use PhpZip\Model\ImmutableZipContainer; |
||||
| 26 | use PhpZip\Model\ZipEntry; |
||||
| 27 | use PhpZip\Util\PackUtil; |
||||
| 28 | |||||
| 29 | /** |
||||
| 30 | * Zip reader. |
||||
| 31 | * |
||||
| 32 | * @author Ne-Lexa [email protected] |
||||
| 33 | * @license MIT |
||||
| 34 | */ |
||||
| 35 | class ZipReader |
||||
| 36 | { |
||||
| 37 | /** @var int file size */ |
||||
| 38 | protected $size; |
||||
| 39 | |||||
| 40 | /** @var resource */ |
||||
| 41 | protected $inStream; |
||||
| 42 | |||||
| 43 | /** @var array */ |
||||
| 44 | protected $options; |
||||
| 45 | |||||
| 46 | /** |
||||
| 47 | * @param resource $inStream |
||||
| 48 | * @param array $options |
||||
| 49 | */ |
||||
| 50 | 159 | public function __construct($inStream, array $options = []) |
|||
| 51 | { |
||||
| 52 | 159 | if (!\is_resource($inStream)) { |
|||
| 53 | 4 | throw new InvalidArgumentException('Stream must be a resource'); |
|||
| 54 | } |
||||
| 55 | 155 | $type = get_resource_type($inStream); |
|||
| 56 | |||||
| 57 | 155 | if ($type !== 'stream') { |
|||
| 58 | 2 | throw new InvalidArgumentException("Invalid resource type {$type}."); |
|||
| 59 | } |
||||
| 60 | 153 | $meta = stream_get_meta_data($inStream); |
|||
| 61 | |||||
| 62 | 153 | $wrapperType = isset($meta['wrapper_type']) ? $meta['wrapper_type'] : 'Unknown'; |
|||
| 63 | 153 | $supportStreamWrapperTypes = ['plainfile', 'PHP', 'user-space']; |
|||
| 64 | |||||
| 65 | 153 | if (!\in_array($wrapperType, $supportStreamWrapperTypes, true)) { |
|||
| 66 | 3 | throw new InvalidArgumentException( |
|||
| 67 | 3 | 'The stream wrapper type "' . $wrapperType . '" is not supported. Support: ' . implode( |
|||
| 68 | 3 | ', ', |
|||
| 69 | $supportStreamWrapperTypes |
||||
| 70 | ) |
||||
| 71 | ); |
||||
| 72 | } |
||||
| 73 | |||||
| 74 | if ( |
||||
| 75 | 150 | $wrapperType === 'plainfile' && |
|||
| 76 | ( |
||||
| 77 | 135 | $meta['stream_type'] === 'dir' || |
|||
| 78 | 150 | (isset($meta['uri']) && is_dir($meta['uri'])) |
|||
| 79 | ) |
||||
| 80 | ) { |
||||
| 81 | 3 | throw new InvalidArgumentException('Directory stream not supported'); |
|||
| 82 | } |
||||
| 83 | |||||
| 84 | 147 | $seekable = $meta['seekable']; |
|||
| 85 | |||||
| 86 | 147 | if (!$seekable) { |
|||
| 87 | throw new InvalidArgumentException('Resource does not support seekable.'); |
||||
| 88 | } |
||||
| 89 | 147 | $this->size = fstat($inStream)['size']; |
|||
| 90 | 147 | $this->inStream = $inStream; |
|||
| 91 | |||||
| 92 | /** @noinspection AdditionOperationOnArraysInspection */ |
||||
| 93 | 147 | $options += $this->getDefaultOptions(); |
|||
| 94 | 147 | $this->options = $options; |
|||
| 95 | 147 | } |
|||
| 96 | |||||
| 97 | /** |
||||
| 98 | * @return array |
||||
| 99 | */ |
||||
| 100 | 147 | protected function getDefaultOptions() |
|||
| 101 | { |
||||
| 102 | return [ |
||||
| 103 | 147 | ZipOptions::CHARSET => null, |
|||
| 104 | ]; |
||||
| 105 | } |
||||
| 106 | |||||
| 107 | /** |
||||
| 108 | * @throws ZipException |
||||
| 109 | * |
||||
| 110 | * @return ImmutableZipContainer |
||||
| 111 | */ |
||||
| 112 | 147 | public function read() |
|||
| 113 | { |
||||
| 114 | 147 | if ($this->size < ZipConstants::END_CD_MIN_LEN) { |
|||
| 115 | 4 | throw new ZipException('Corrupt zip file'); |
|||
| 116 | } |
||||
| 117 | |||||
| 118 | 143 | $endOfCentralDirectory = $this->readEndOfCentralDirectory(); |
|||
| 119 | 137 | $entries = $this->readCentralDirectory($endOfCentralDirectory); |
|||
| 120 | |||||
| 121 | 136 | return new ImmutableZipContainer($entries, $endOfCentralDirectory->getComment()); |
|||
| 122 | } |
||||
| 123 | |||||
| 124 | /** |
||||
| 125 | * @return array |
||||
| 126 | */ |
||||
| 127 | 9 | public function getStreamMetaData() |
|||
| 128 | { |
||||
| 129 | 9 | return stream_get_meta_data($this->inStream); |
|||
| 130 | } |
||||
| 131 | |||||
| 132 | /** |
||||
| 133 | * Read End of central directory record. |
||||
| 134 | * |
||||
| 135 | * end of central dir signature 4 bytes (0x06054b50) |
||||
| 136 | * number of this disk 2 bytes |
||||
| 137 | * number of the disk with the |
||||
| 138 | * start of the central directory 2 bytes |
||||
| 139 | * total number of entries in the |
||||
| 140 | * central directory on this disk 2 bytes |
||||
| 141 | * total number of entries in |
||||
| 142 | * the central directory 2 bytes |
||||
| 143 | * size of the central directory 4 bytes |
||||
| 144 | * offset of start of central |
||||
| 145 | * directory with respect to |
||||
| 146 | * the starting disk number 4 bytes |
||||
| 147 | * .ZIP file comment length 2 bytes |
||||
| 148 | * .ZIP file comment (variable size) |
||||
| 149 | * |
||||
| 150 | * @throws ZipException |
||||
| 151 | * |
||||
| 152 | * @return EndOfCentralDirectory |
||||
| 153 | */ |
||||
| 154 | 143 | protected function readEndOfCentralDirectory() |
|||
| 155 | { |
||||
| 156 | 143 | if (!$this->findEndOfCentralDirectory()) { |
|||
| 157 | 6 | throw new ZipException('Invalid zip file. The end of the central directory could not be found.'); |
|||
| 158 | } |
||||
| 159 | |||||
| 160 | 137 | $positionECD = ftell($this->inStream) - 4; |
|||
| 161 | 137 | $sizeECD = $this->size - ftell($this->inStream); |
|||
| 162 | 137 | $buffer = fread($this->inStream, $sizeECD); |
|||
| 163 | |||||
| 164 | 137 | $unpack = unpack( |
|||
| 165 | 'vdiskNo/vcdDiskNo/vcdEntriesDisk/' . |
||||
| 166 | 137 | 'vcdEntries/VcdSize/VcdPos/vcommentLength', |
|||
| 167 | 137 | substr($buffer, 0, 18) |
|||
| 168 | ); |
||||
| 169 | |||||
| 170 | if ( |
||||
| 171 | 137 | $unpack['diskNo'] !== 0 || |
|||
| 172 | 137 | $unpack['cdDiskNo'] !== 0 || |
|||
| 173 | 137 | $unpack['cdEntriesDisk'] !== $unpack['cdEntries'] |
|||
| 174 | ) { |
||||
| 175 | throw new ZipException( |
||||
| 176 | 'ZIP file spanning/splitting is not supported!' |
||||
| 177 | ); |
||||
| 178 | } |
||||
| 179 | // .ZIP file comment (variable sizeECD) |
||||
| 180 | 137 | $comment = null; |
|||
| 181 | |||||
| 182 | 137 | if ($unpack['commentLength'] > 0) { |
|||
| 183 | 6 | $comment = substr($buffer, 18, $unpack['commentLength']); |
|||
| 184 | } |
||||
| 185 | |||||
| 186 | // Check for ZIP64 End Of Central Directory Locator exists. |
||||
| 187 | 137 | $zip64ECDLocatorPosition = $positionECD - ZipConstants::ZIP64_END_CD_LOC_LEN; |
|||
| 188 | 137 | fseek($this->inStream, $zip64ECDLocatorPosition); |
|||
| 189 | // zip64 end of central dir locator |
||||
| 190 | // signature 4 bytes (0x07064b50) |
||||
| 191 | 137 | if ($zip64ECDLocatorPosition > 0 && unpack( |
|||
| 192 | 135 | 'V', |
|||
| 193 | 135 | fread($this->inStream, 4) |
|||
| 194 | 137 | )[1] === ZipConstants::ZIP64_END_CD_LOC) { |
|||
| 195 | 1 | if (!$this->isZip64Support()) { |
|||
| 196 | throw new ZipException('ZIP64 not supported this archive.'); |
||||
| 197 | } |
||||
| 198 | |||||
| 199 | 1 | $positionECD = $this->findZip64ECDPosition(); |
|||
| 200 | 1 | $endCentralDirectory = $this->readZip64EndOfCentralDirectory($positionECD); |
|||
| 201 | 1 | $endCentralDirectory->setComment($comment); |
|||
| 202 | } else { |
||||
| 203 | 136 | $endCentralDirectory = new EndOfCentralDirectory( |
|||
| 204 | 136 | $unpack['cdEntries'], |
|||
| 205 | 136 | $unpack['cdPos'], |
|||
| 206 | 136 | $unpack['cdSize'], |
|||
| 207 | 136 | false, |
|||
| 208 | $comment |
||||
| 209 | ); |
||||
| 210 | } |
||||
| 211 | |||||
| 212 | 137 | return $endCentralDirectory; |
|||
| 213 | } |
||||
| 214 | |||||
| 215 | /** |
||||
| 216 | * @return bool |
||||
| 217 | */ |
||||
| 218 | 143 | protected function findEndOfCentralDirectory() |
|||
| 219 | { |
||||
| 220 | 143 | $max = $this->size - ZipConstants::END_CD_MIN_LEN; |
|||
| 221 | 143 | $min = $max >= 0xffff ? $max - 0xffff : 0; |
|||
| 222 | // Search for End of central directory record. |
||||
| 223 | 143 | for ($position = $max; $position >= $min; $position--) { |
|||
| 224 | 143 | fseek($this->inStream, $position); |
|||
| 225 | // end of central dir signature 4 bytes (0x06054b50) |
||||
| 226 | 143 | if (unpack('V', fread($this->inStream, 4))[1] !== ZipConstants::END_CD) { |
|||
| 227 | 12 | continue; |
|||
| 228 | } |
||||
| 229 | |||||
| 230 | 137 | return true; |
|||
| 231 | } |
||||
| 232 | |||||
| 233 | 6 | return false; |
|||
| 234 | } |
||||
| 235 | |||||
| 236 | /** |
||||
| 237 | * Read Zip64 end of central directory locator and returns |
||||
| 238 | * Zip64 end of central directory position. |
||||
| 239 | * |
||||
| 240 | * number of the disk with the |
||||
| 241 | * start of the zip64 end of |
||||
| 242 | * central directory 4 bytes |
||||
| 243 | * relative offset of the zip64 |
||||
| 244 | * end of central directory record 8 bytes |
||||
| 245 | * total number of disks 4 bytes |
||||
| 246 | * |
||||
| 247 | * @throws ZipException |
||||
| 248 | * |
||||
| 249 | * @return int Zip64 End Of Central Directory position |
||||
| 250 | */ |
||||
| 251 | 1 | protected function findZip64ECDPosition() |
|||
| 252 | { |
||||
| 253 | 1 | $diskNo = unpack('V', fread($this->inStream, 4))[1]; |
|||
| 254 | 1 | $zip64ECDPos = PackUtil::unpackLongLE(fread($this->inStream, 8)); |
|||
| 255 | 1 | $totalDisks = unpack('V', fread($this->inStream, 4))[1]; |
|||
| 256 | |||||
| 257 | 1 | if ($diskNo !== 0 || $totalDisks > 1) { |
|||
| 258 | throw new ZipException('ZIP file spanning/splitting is not supported!'); |
||||
| 259 | } |
||||
| 260 | |||||
| 261 | 1 | return $zip64ECDPos; |
|||
| 262 | } |
||||
| 263 | |||||
| 264 | /** |
||||
| 265 | * Read zip64 end of central directory locator and zip64 end |
||||
| 266 | * of central directory record. |
||||
| 267 | * |
||||
| 268 | * zip64 end of central dir |
||||
| 269 | * signature 4 bytes (0x06064b50) |
||||
| 270 | * size of zip64 end of central |
||||
| 271 | * directory record 8 bytes |
||||
| 272 | * version made by 2 bytes |
||||
| 273 | * version needed to extract 2 bytes |
||||
| 274 | * number of this disk 4 bytes |
||||
| 275 | * number of the disk with the |
||||
| 276 | * start of the central directory 4 bytes |
||||
| 277 | * total number of entries in the |
||||
| 278 | * central directory on this disk 8 bytes |
||||
| 279 | * total number of entries in the |
||||
| 280 | * central directory 8 bytes |
||||
| 281 | * size of the central directory 8 bytes |
||||
| 282 | * offset of start of central |
||||
| 283 | * directory with respect to |
||||
| 284 | * the starting disk number 8 bytes |
||||
| 285 | * zip64 extensible data sector (variable size) |
||||
| 286 | * |
||||
| 287 | * @param int $zip64ECDPosition |
||||
| 288 | * |
||||
| 289 | * @throws ZipException |
||||
| 290 | * |
||||
| 291 | * @return EndOfCentralDirectory |
||||
| 292 | */ |
||||
| 293 | 1 | protected function readZip64EndOfCentralDirectory($zip64ECDPosition) |
|||
| 294 | { |
||||
| 295 | 1 | fseek($this->inStream, $zip64ECDPosition); |
|||
| 296 | |||||
| 297 | 1 | $buffer = fread($this->inStream, ZipConstants::ZIP64_END_OF_CD_LEN); |
|||
| 298 | |||||
| 299 | 1 | if (unpack('V', $buffer)[1] !== ZipConstants::ZIP64_END_CD) { |
|||
| 300 | throw new ZipException('Expected ZIP64 End Of Central Directory Record!'); |
||||
| 301 | } |
||||
| 302 | |||||
| 303 | 1 | $data = unpack( |
|||
| 304 | // 'Psize/vversionMadeBy/vextractVersion/' . |
||||
| 305 | 1 | 'VdiskNo/VcdDiskNo', |
|||
| 306 | 1 | substr($buffer, 16, 8) |
|||
| 307 | ); |
||||
| 308 | |||||
| 309 | 1 | $cdEntriesDisk = PackUtil::unpackLongLE(substr($buffer, 24, 8)); |
|||
| 310 | 1 | $entryCount = PackUtil::unpackLongLE(substr($buffer, 32, 8)); |
|||
| 311 | 1 | $cdSize = PackUtil::unpackLongLE(substr($buffer, 40, 8)); |
|||
| 312 | 1 | $cdPos = PackUtil::unpackLongLE(substr($buffer, 48, 8)); |
|||
| 313 | |||||
| 314 | // $platform = ZipPlatform::fromValue(($data['versionMadeBy'] & 0xFF00) >> 8); |
||||
| 315 | // $softwareVersion = $data['versionMadeBy'] & 0x00FF; |
||||
| 316 | |||||
| 317 | 1 | if ($data['diskNo'] !== 0 || $data['cdDiskNo'] !== 0 || $entryCount !== $cdEntriesDisk) { |
|||
| 318 | throw new ZipException('ZIP file spanning/splitting is not supported!'); |
||||
| 319 | } |
||||
| 320 | |||||
| 321 | 1 | if ($entryCount < 0 || $entryCount > 0x7fffffff) { |
|||
| 322 | throw new ZipException('Total Number Of Entries In The Central Directory out of range!'); |
||||
| 323 | } |
||||
| 324 | |||||
| 325 | // skip zip64 extensible data sector (variable sizeEndCD) |
||||
| 326 | |||||
| 327 | 1 | return new EndOfCentralDirectory( |
|||
| 328 | 1 | $entryCount, |
|||
| 329 | $cdPos, |
||||
| 330 | $cdSize, |
||||
| 331 | 1 | true |
|||
| 332 | ); |
||||
| 333 | } |
||||
| 334 | |||||
| 335 | /** |
||||
| 336 | * Reads the central directory from the given seekable byte channel |
||||
| 337 | * and populates the internal tables with ZipEntry instances. |
||||
| 338 | * |
||||
| 339 | * The ZipEntry's will know all data that can be obtained from the |
||||
| 340 | * central directory alone, but not the data that requires the local |
||||
| 341 | * file header or additional data to be read. |
||||
| 342 | * |
||||
| 343 | * @param EndOfCentralDirectory $endCD |
||||
| 344 | * |
||||
| 345 | * @throws ZipException |
||||
| 346 | * |
||||
| 347 | * @return ZipEntry[] |
||||
| 348 | */ |
||||
| 349 | 137 | protected function readCentralDirectory(EndOfCentralDirectory $endCD) |
|||
| 350 | { |
||||
| 351 | 137 | $entries = []; |
|||
| 352 | |||||
| 353 | 137 | $cdOffset = $endCD->getCdOffset(); |
|||
| 354 | 137 | fseek($this->inStream, $cdOffset); |
|||
| 355 | |||||
| 356 | 137 | if (!($cdStream = fopen('php://temp', 'w+b'))) { |
|||
| 357 | // @codeCoverageIgnoreStart |
||||
| 358 | throw new ZipException('A temporary resource cannot be opened for writing.'); |
||||
| 359 | 137 | // @codeCoverageIgnoreEnd |
|||
| 360 | 137 | } |
|||
| 361 | 137 | stream_copy_to_stream($this->inStream, $cdStream, $endCD->getCdSize()); |
|||
| 362 | 135 | rewind($cdStream); |
|||
| 363 | for ($numEntries = $endCD->getEntryCount(); $numEntries > 0; $numEntries--) { |
||||
| 364 | 134 | $zipEntry = $this->readZipEntry($cdStream); |
|||
| 365 | |||||
| 366 | $entryName = $zipEntry->getName(); |
||||
| 367 | 134 | ||||
| 368 | /** @var UnicodePathExtraField|null $unicodePathExtraField */ |
||||
| 369 | 134 | $unicodePathExtraField = $zipEntry->getExtraField(UnicodePathExtraField::HEADER_ID); |
|||
| 370 | |||||
| 371 | if ($unicodePathExtraField !== null && $unicodePathExtraField->getCrc32() === crc32($entryName)) { |
||||
| 372 | $unicodePath = $unicodePathExtraField->getUnicodeValue(); |
||||
| 373 | |||||
| 374 | if ($unicodePath !== null) { |
||||
| 375 | $unicodePath = str_replace('\\', '/', $unicodePath); |
||||
| 376 | |||||
| 377 | if ( |
||||
| 378 | $unicodePath !== '' && |
||||
| 379 | substr_count($entryName, '/') === substr_count($unicodePath, '/') |
||||
| 380 | ) { |
||||
| 381 | $entryName = $unicodePath; |
||||
| 382 | } |
||||
| 383 | } |
||||
| 384 | 134 | } |
|||
| 385 | |||||
| 386 | $entries[$entryName] = $zipEntry; |
||||
| 387 | 136 | } |
|||
| 388 | |||||
| 389 | return $entries; |
||||
| 390 | } |
||||
| 391 | |||||
| 392 | /** |
||||
| 393 | * Read central directory entry. |
||||
| 394 | * |
||||
| 395 | * central file header signature 4 bytes (0x02014b50) |
||||
| 396 | * version made by 2 bytes |
||||
| 397 | * version needed to extract 2 bytes |
||||
| 398 | * general purpose bit flag 2 bytes |
||||
| 399 | * compression method 2 bytes |
||||
| 400 | * last mod file time 2 bytes |
||||
| 401 | * last mod file date 2 bytes |
||||
| 402 | * crc-32 4 bytes |
||||
| 403 | * compressed size 4 bytes |
||||
| 404 | * uncompressed size 4 bytes |
||||
| 405 | * file name length 2 bytes |
||||
| 406 | * extra field length 2 bytes |
||||
| 407 | * file comment length 2 bytes |
||||
| 408 | * disk number start 2 bytes |
||||
| 409 | * internal file attributes 2 bytes |
||||
| 410 | * external file attributes 4 bytes |
||||
| 411 | * relative offset of local header 4 bytes |
||||
| 412 | * |
||||
| 413 | * file name (variable size) |
||||
| 414 | * extra field (variable size) |
||||
| 415 | * file comment (variable size) |
||||
| 416 | * |
||||
| 417 | * @param resource $stream |
||||
| 418 | * |
||||
| 419 | * @throws ZipException |
||||
| 420 | * |
||||
| 421 | 135 | * @return ZipEntry |
|||
| 422 | */ |
||||
| 423 | 135 | protected function readZipEntry($stream) |
|||
| 424 | 1 | { |
|||
| 425 | if (unpack('V', fread($stream, 4))[1] !== ZipConstants::CENTRAL_FILE_HEADER) { |
||||
| 426 | throw new ZipException('Corrupt zip file. Cannot read zip entry.'); |
||||
| 427 | 134 | } |
|||
| 428 | |||||
| 429 | $unpack = unpack( |
||||
| 430 | 'vversionMadeBy/vversionNeededToExtract/' . |
||||
| 431 | 'vgeneralPurposeBitFlag/vcompressionMethod/' . |
||||
| 432 | 'VlastModFile/Vcrc/VcompressedSize/' . |
||||
| 433 | 134 | 'VuncompressedSize/vfileNameLength/vextraFieldLength/' . |
|||
| 434 | 134 | 'vfileCommentLength/vdiskNumberStart/vinternalFileAttributes/' . |
|||
| 435 | 'VexternalFileAttributes/VoffsetLocalHeader', |
||||
| 436 | fread($stream, 42) |
||||
| 437 | 134 | ); |
|||
| 438 | |||||
| 439 | if ($unpack['diskNumberStart'] !== 0) { |
||||
| 440 | throw new ZipException('ZIP file spanning/splitting is not supported!'); |
||||
| 441 | 134 | } |
|||
| 442 | 134 | ||||
| 443 | $generalPurposeBitFlags = $unpack['generalPurposeBitFlag']; |
||||
| 444 | 134 | $isUtf8 = ($generalPurposeBitFlags & GeneralPurposeBitFlag::UTF8) !== 0; |
|||
| 445 | |||||
| 446 | 134 | $name = fread($stream, $unpack['fileNameLength']); |
|||
| 447 | 134 | ||||
| 448 | $createdOS = ($unpack['versionMadeBy'] & 0xFF00) >> 8; |
||||
| 449 | 134 | $softwareVersion = $unpack['versionMadeBy'] & 0x00FF; |
|||
| 450 | 134 | ||||
| 451 | $extractedOS = ($unpack['versionNeededToExtract'] & 0xFF00) >> 8; |
||||
| 452 | 134 | $extractVersion = $unpack['versionNeededToExtract'] & 0x00FF; |
|||
| 453 | |||||
| 454 | 134 | $dosTime = $unpack['lastModFile']; |
|||
| 455 | |||||
| 456 | 134 | $comment = null; |
|||
| 457 | 3 | ||||
| 458 | if ($unpack['fileCommentLength'] > 0) { |
||||
| 459 | $comment = fread($stream, $unpack['fileCommentLength']); |
||||
| 460 | } |
||||
| 461 | 134 | ||||
| 462 | // decode code page names |
||||
| 463 | 134 | $fallbackCharset = null; |
|||
| 464 | |||||
| 465 | if (!$isUtf8 && isset($this->options[ZipOptions::CHARSET])) { |
||||
| 466 | $charset = $this->options[ZipOptions::CHARSET]; |
||||
| 467 | |||||
| 468 | $fallbackCharset = $charset; |
||||
| 469 | $name = DosCodePage::toUTF8($name, $charset); |
||||
| 470 | |||||
| 471 | if ($comment !== null) { |
||||
| 472 | $comment = DosCodePage::toUTF8($comment, $charset); |
||||
| 473 | } |
||||
| 474 | 134 | } |
|||
| 475 | 134 | ||||
| 476 | 134 | $zipEntry = ZipEntry::create( |
|||
| 477 | 134 | $name, |
|||
| 478 | 134 | $createdOS, |
|||
| 479 | 134 | $extractedOS, |
|||
| 480 | 134 | $softwareVersion, |
|||
| 481 | 134 | $extractVersion, |
|||
| 482 | 134 | $unpack['compressionMethod'], |
|||
| 483 | 134 | $generalPurposeBitFlags, |
|||
| 484 | 134 | $dosTime, |
|||
| 485 | 134 | $unpack['crc'], |
|||
| 486 | 134 | $unpack['compressedSize'], |
|||
| 487 | 134 | $unpack['uncompressedSize'], |
|||
| 488 | 134 | $unpack['internalFileAttributes'], |
|||
| 489 | 134 | $unpack['externalFileAttributes'], |
|||
| 490 | 134 | $unpack['offsetLocalHeader'], |
|||
| 491 | $comment, |
||||
| 492 | $fallbackCharset |
||||
| 493 | 134 | ); |
|||
| 494 | 18 | ||||
| 495 | 18 | if ($unpack['extraFieldLength'] > 0) { |
|||
| 496 | $this->parseExtraFields( |
||||
| 497 | 18 | fread($stream, $unpack['extraFieldLength']), |
|||
| 498 | $zipEntry, |
||||
| 499 | false |
||||
| 500 | ); |
||||
| 501 | 18 | ||||
| 502 | /** @var Zip64ExtraField|null $extraZip64 */ |
||||
| 503 | 18 | $extraZip64 = $zipEntry->getCdExtraField(Zip64ExtraField::HEADER_ID); |
|||
| 504 | |||||
| 505 | if ($extraZip64 !== null) { |
||||
| 506 | $this->handleZip64Extra($extraZip64, $zipEntry); |
||||
| 507 | } |
||||
| 508 | 134 | } |
|||
| 509 | 134 | ||||
| 510 | 134 | $this->loadLocalExtraFields($zipEntry); |
|||
| 511 | $this->handleExtraEncryptionFields($zipEntry); |
||||
| 512 | 134 | $this->handleExtraFields($zipEntry); |
|||
| 513 | |||||
| 514 | return $zipEntry; |
||||
| 515 | } |
||||
| 516 | |||||
| 517 | /** |
||||
| 518 | * @param string $buffer |
||||
| 519 | * @param ZipEntry $zipEntry |
||||
| 520 | * @param bool $local |
||||
| 521 | * |
||||
| 522 | 18 | * @return ExtraFieldsCollection |
|||
| 523 | */ |
||||
| 524 | 18 | protected function parseExtraFields($buffer, ZipEntry $zipEntry, $local = false) |
|||
| 525 | 17 | { |
|||
| 526 | 18 | $collection = $local ? |
|||
| 527 | $zipEntry->getLocalExtraFields() : |
||||
| 528 | 18 | $zipEntry->getCdExtraFields(); |
|||
| 529 | 18 | ||||
| 530 | 18 | if (!empty($buffer)) { |
|||
| 531 | $pos = 0; |
||||
| 532 | 18 | $endPos = \strlen($buffer); |
|||
| 533 | |||||
| 534 | 18 | while ($endPos - $pos >= 4) { |
|||
| 535 | 18 | /** @var int[] $data */ |
|||
| 536 | $data = unpack('vheaderId/vdataSize', substr($buffer, $pos, 4)); |
||||
| 537 | 18 | $pos += 4; |
|||
| 538 | 1 | ||||
| 539 | if ($endPos - $pos - $data['dataSize'] < 0) { |
||||
| 540 | 18 | break; |
|||
| 541 | 18 | } |
|||
| 542 | $bufferData = substr($buffer, $pos, $data['dataSize']); |
||||
| 543 | $headerId = $data['headerId']; |
||||
| 544 | 18 | ||||
| 545 | /** @var string|ZipExtraField|null $className */ |
||||
| 546 | $className = ZipExtraDriver::getClassNameOrNull($headerId); |
||||
| 547 | 18 | ||||
| 548 | try { |
||||
| 549 | 18 | if ($className !== null) { |
|||
| 550 | 17 | try { |
|||
| 551 | 18 | $extraField = $local ? |
|||
| 552 | \call_user_func([$className, 'unpackLocalFileData'], $bufferData, $zipEntry) : |
||||
| 553 | \call_user_func([$className, 'unpackCentralDirData'], $bufferData, $zipEntry); |
||||
| 554 | 18 | } catch (\Throwable $e) { |
|||
| 555 | // skip errors while parsing invalid data |
||||
| 556 | continue; |
||||
| 557 | 2 | } |
|||
| 558 | } else { |
||||
| 559 | 18 | $extraField = new UnrecognizedExtraField($headerId, $bufferData); |
|||
| 560 | 18 | } |
|||
| 561 | 18 | $collection->add($extraField); |
|||
| 562 | } finally { |
||||
| 563 | $pos += $data['dataSize']; |
||||
| 564 | } |
||||
| 565 | } |
||||
| 566 | 18 | } |
|||
| 567 | |||||
| 568 | return $collection; |
||||
| 569 | } |
||||
| 570 | |||||
| 571 | /** |
||||
| 572 | * @param Zip64ExtraField $extraZip64 |
||||
| 573 | * @param ZipEntry $zipEntry |
||||
| 574 | */ |
||||
| 575 | protected function handleZip64Extra(Zip64ExtraField $extraZip64, ZipEntry $zipEntry) |
||||
| 576 | { |
||||
| 577 | $uncompressedSize = $extraZip64->getUncompressedSize(); |
||||
| 578 | $compressedSize = $extraZip64->getCompressedSize(); |
||||
| 579 | $localHeaderOffset = $extraZip64->getLocalHeaderOffset(); |
||||
| 580 | |||||
| 581 | if ($uncompressedSize !== null) { |
||||
| 582 | $zipEntry->setUncompressedSize($uncompressedSize); |
||||
| 583 | } |
||||
| 584 | |||||
| 585 | if ($compressedSize !== null) { |
||||
| 586 | $zipEntry->setCompressedSize($compressedSize); |
||||
| 587 | } |
||||
| 588 | |||||
| 589 | if ($localHeaderOffset !== null) { |
||||
| 590 | $zipEntry->setLocalHeaderOffset($localHeaderOffset); |
||||
| 591 | } |
||||
| 592 | } |
||||
| 593 | |||||
| 594 | /** |
||||
| 595 | * Read Local File Header. |
||||
| 596 | * |
||||
| 597 | * local file header signature 4 bytes (0x04034b50) |
||||
| 598 | * version needed to extract 2 bytes |
||||
| 599 | * general purpose bit flag 2 bytes |
||||
| 600 | * compression method 2 bytes |
||||
| 601 | * last mod file time 2 bytes |
||||
| 602 | * last mod file date 2 bytes |
||||
| 603 | * crc-32 4 bytes |
||||
| 604 | * compressed size 4 bytes |
||||
| 605 | * uncompressed size 4 bytes |
||||
| 606 | * file name length 2 bytes |
||||
| 607 | * extra field length 2 bytes |
||||
| 608 | * file name (variable size) |
||||
| 609 | * extra field (variable size) |
||||
| 610 | * |
||||
| 611 | * @param ZipEntry $entry |
||||
| 612 | * |
||||
| 613 | 134 | * @throws ZipException |
|||
| 614 | */ |
||||
| 615 | 134 | protected function loadLocalExtraFields(ZipEntry $entry) |
|||
| 616 | { |
||||
| 617 | 134 | $offsetLocalHeader = $entry->getLocalHeaderOffset(); |
|||
| 618 | |||||
| 619 | 134 | fseek($this->inStream, $offsetLocalHeader); |
|||
| 620 | |||||
| 621 | if (unpack('V', fread($this->inStream, 4))[1] !== ZipConstants::LOCAL_FILE_HEADER) { |
||||
| 622 | throw new ZipException(sprintf('%s (expected Local File Header)', $entry->getName())); |
||||
| 623 | 134 | } |
|||
| 624 | 134 | ||||
| 625 | 134 | fseek($this->inStream, $offsetLocalHeader + ZipConstants::LFH_FILENAME_LENGTH_POS); |
|||
| 626 | 134 | $unpack = unpack('vfileNameLength/vextraFieldLength', fread($this->inStream, 4)); |
|||
| 627 | 134 | $offsetData = ftell($this->inStream) |
|||
| 628 | + $unpack['fileNameLength'] |
||||
| 629 | 134 | + $unpack['extraFieldLength']; |
|||
| 630 | |||||
| 631 | 134 | fseek($this->inStream, $unpack['fileNameLength'], \SEEK_CUR); |
|||
| 632 | 17 | ||||
| 633 | 17 | if ($unpack['extraFieldLength'] > 0) { |
|||
| 634 | $this->parseExtraFields( |
||||
| 635 | 17 | fread($this->inStream, $unpack['extraFieldLength']), |
|||
| 636 | $entry, |
||||
| 637 | true |
||||
| 638 | ); |
||||
| 639 | 134 | } |
|||
| 640 | 134 | ||||
| 641 | 134 | $zipData = new ZipSourceFileData($this, $entry, $offsetData); |
|||
| 642 | $entry->setData($zipData); |
||||
| 643 | } |
||||
| 644 | |||||
| 645 | /** |
||||
| 646 | * @param ZipEntry $zipEntry |
||||
| 647 | * |
||||
| 648 | 134 | * @throws ZipException |
|||
| 649 | */ |
||||
| 650 | 134 | private function handleExtraEncryptionFields(ZipEntry $zipEntry) |
|||
| 651 | 11 | { |
|||
| 652 | if ($zipEntry->isEncrypted()) { |
||||
| 653 | 9 | if ($zipEntry->getCompressionMethod() === ZipCompressionMethod::WINZIP_AES) { |
|||
| 654 | /** @var WinZipAesExtraField|null $extraField */ |
||||
| 655 | 9 | $extraField = $zipEntry->getExtraField(WinZipAesExtraField::HEADER_ID); |
|||
| 656 | |||||
| 657 | if ($extraField === null) { |
||||
| 658 | throw new ZipException( |
||||
| 659 | sprintf( |
||||
| 660 | 'Extra field 0x%04x (WinZip-AES Encryption) expected for compression method %d', |
||||
| 661 | WinZipAesExtraField::HEADER_ID, |
||||
| 662 | $zipEntry->getCompressionMethod() |
||||
| 663 | ) |
||||
| 664 | 9 | ); |
|||
| 665 | 9 | } |
|||
| 666 | $zipEntry->setCompressionMethod($extraField->getCompressionMethod()); |
||||
| 667 | 5 | $zipEntry->setEncryptionMethod($extraField->getEncryptionMethod()); |
|||
| 668 | } else { |
||||
| 669 | $zipEntry->setEncryptionMethod(ZipEncryptionMethod::PKWARE); |
||||
| 670 | 134 | } |
|||
| 671 | } |
||||
| 672 | } |
||||
| 673 | |||||
| 674 | /** |
||||
| 675 | * Handle extra data in zip records. |
||||
| 676 | * |
||||
| 677 | * This is a special method in which you can process ExtraField |
||||
| 678 | * and make changes to ZipEntry. |
||||
| 679 | * |
||||
| 680 | 134 | * @param ZipEntry $zipEntry |
|||
| 681 | */ |
||||
| 682 | 134 | protected function handleExtraFields(ZipEntry $zipEntry) |
|||
|
0 ignored issues
–
show
|
|||||
| 683 | { |
||||
| 684 | } |
||||
| 685 | |||||
| 686 | /** |
||||
| 687 | * @param ZipSourceFileData $zipFileData |
||||
| 688 | * |
||||
| 689 | * @throws ZipException |
||||
| 690 | * @throws Crc32Exception |
||||
| 691 | * |
||||
| 692 | 74 | * @return resource |
|||
| 693 | */ |
||||
| 694 | 74 | public function getEntryStream(ZipSourceFileData $zipFileData) |
|||
| 695 | 74 | { |
|||
| 696 | 73 | $outStream = fopen('php://temp', 'w+b'); |
|||
| 697 | $this->copyUncompressedDataToStream($zipFileData, $outStream); |
||||
|
0 ignored issues
–
show
It seems like
$outStream can also be of type false; however, parameter $outStream of PhpZip\IO\ZipReader::cop...ompressedDataToStream() does only seem to accept resource, maybe add an additional type check?
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
Loading history...
|
|||||
| 698 | 73 | rewind($outStream); |
|||
| 699 | |||||
| 700 | return $outStream; |
||||
| 701 | } |
||||
| 702 | |||||
| 703 | /** |
||||
| 704 | * @param ZipSourceFileData $zipFileData |
||||
| 705 | * @param resource $outStream |
||||
| 706 | * |
||||
| 707 | * @throws Crc32Exception |
||||
| 708 | 79 | * @throws ZipException |
|||
| 709 | */ |
||||
| 710 | 79 | public function copyUncompressedDataToStream(ZipSourceFileData $zipFileData, $outStream) |
|||
| 711 | { |
||||
| 712 | if (!\is_resource($outStream)) { |
||||
| 713 | throw new InvalidArgumentException('outStream is not resource'); |
||||
| 714 | 79 | } |
|||
| 715 | |||||
| 716 | $entry = $zipFileData->getSourceEntry(); |
||||
| 717 | |||||
| 718 | // if ($entry->isDirectory()) { |
||||
| 719 | // throw new InvalidArgumentException('Streams not supported for directories'); |
||||
| 720 | 79 | // } |
|||
| 721 | |||||
| 722 | if ($entry->isStrongEncryption()) { |
||||
| 723 | throw new ZipException('Not support encryption zip.'); |
||||
| 724 | 79 | } |
|||
| 725 | |||||
| 726 | 79 | $compressionMethod = $entry->getCompressionMethod(); |
|||
| 727 | |||||
| 728 | 79 | fseek($this->inStream, $zipFileData->getOffset()); |
|||
| 729 | |||||
| 730 | 79 | $filters = []; |
|||
| 731 | 79 | ||||
| 732 | $skipCheckCrc = false; |
||||
| 733 | 79 | $isEncrypted = $entry->isEncrypted(); |
|||
| 734 | 10 | ||||
| 735 | if ($isEncrypted) { |
||||
| 736 | if ($entry->getPassword() === null) { |
||||
| 737 | throw new ZipException('Can not password from entry ' . $entry->getName()); |
||||
| 738 | 10 | } |
|||
| 739 | |||||
| 740 | 8 | if (ZipEncryptionMethod::isWinZipAesMethod($entry->getEncryptionMethod())) { |
|||
| 741 | /** @var WinZipAesExtraField|null $winZipAesExtra */ |
||||
| 742 | 8 | $winZipAesExtra = $entry->getExtraField(WinZipAesExtraField::HEADER_ID); |
|||
| 743 | |||||
| 744 | if ($winZipAesExtra === null) { |
||||
| 745 | throw new ZipException( |
||||
| 746 | sprintf('WinZip AES must contain the extra field %s', WinZipAesExtraField::HEADER_ID) |
||||
| 747 | 8 | ); |
|||
| 748 | } |
||||
| 749 | 8 | $compressionMethod = $winZipAesExtra->getCompressionMethod(); |
|||
| 750 | 8 | ||||
| 751 | WinZipAesDecryptionStreamFilter::register(); |
||||
| 752 | 8 | $cipherFilterName = WinZipAesDecryptionStreamFilter::FILTER_NAME; |
|||
| 753 | 8 | ||||
| 754 | if ($winZipAesExtra->isV2()) { |
||||
| 755 | $skipCheckCrc = true; |
||||
| 756 | 5 | } |
|||
| 757 | 5 | } else { |
|||
| 758 | PKDecryptionStreamFilter::register(); |
||||
| 759 | 10 | $cipherFilterName = PKDecryptionStreamFilter::FILTER_NAME; |
|||
| 760 | 10 | } |
|||
| 761 | $encContextFilter = stream_filter_append( |
||||
| 762 | 10 | $this->inStream, |
|||
| 763 | $cipherFilterName, |
||||
| 764 | 10 | \STREAM_FILTER_READ, |
|||
| 765 | [ |
||||
| 766 | 'entry' => $entry, |
||||
| 767 | ] |
||||
| 768 | 10 | ); |
|||
| 769 | |||||
| 770 | if (!$encContextFilter) { |
||||
|
0 ignored issues
–
show
|
|||||
| 771 | 10 | throw new \RuntimeException('Not apply filter ' . $cipherFilterName); |
|||
| 772 | } |
||||
| 773 | $filters[] = $encContextFilter; |
||||
| 774 | } |
||||
| 775 | 79 | ||||
| 776 | 79 | // hack, see https://groups.google.com/forum/#!topic/alt.comp.lang.php/37_JZeW63uc |
|||
| 777 | 79 | $pos = ftell($this->inStream); |
|||
| 778 | rewind($this->inStream); |
||||
| 779 | 79 | fseek($this->inStream, $pos); |
|||
| 780 | 79 | ||||
| 781 | $contextDecompress = null; |
||||
| 782 | switch ($compressionMethod) { |
||||
| 783 | 59 | case ZipCompressionMethod::STORED: |
|||
| 784 | // file without compression, do nothing |
||||
| 785 | break; |
||||
| 786 | 28 | ||||
| 787 | 28 | case ZipCompressionMethod::DEFLATED: |
|||
| 788 | 28 | if (!($contextDecompress = stream_filter_append( |
|||
| 789 | 28 | $this->inStream, |
|||
| 790 | 'zlib.inflate', |
||||
| 791 | \STREAM_FILTER_READ |
||||
| 792 | ))) { |
||||
| 793 | 28 | throw new \RuntimeException('Could not append filter "zlib.inflate" to stream'); |
|||
| 794 | } |
||||
| 795 | 28 | $filters[] = $contextDecompress; |
|||
| 796 | |||||
| 797 | break; |
||||
| 798 | 4 | ||||
| 799 | 4 | case ZipCompressionMethod::BZIP2: |
|||
| 800 | 4 | if (!($contextDecompress = stream_filter_append( |
|||
| 801 | 4 | $this->inStream, |
|||
| 802 | 'bzip2.decompress', |
||||
| 803 | \STREAM_FILTER_READ |
||||
| 804 | ))) { |
||||
| 805 | 4 | throw new \RuntimeException('Could not append filter "bzip2.decompress" to stream'); |
|||
| 806 | } |
||||
| 807 | 4 | $filters[] = $contextDecompress; |
|||
| 808 | |||||
| 809 | break; |
||||
| 810 | |||||
| 811 | default: |
||||
| 812 | throw new ZipException( |
||||
| 813 | sprintf( |
||||
| 814 | '%s (compression method %d (%s) is not supported)', |
||||
| 815 | $entry->getName(), |
||||
| 816 | $compressionMethod, |
||||
| 817 | ZipCompressionMethod::getCompressionMethodName($compressionMethod) |
||||
| 818 | ) |
||||
| 819 | ); |
||||
| 820 | 79 | } |
|||
| 821 | |||||
| 822 | 79 | $limit = $zipFileData->getUncompressedSize(); |
|||
| 823 | 79 | ||||
| 824 | $offset = 0; |
||||
| 825 | $chunkSize = 8192; |
||||
| 826 | 79 | ||||
| 827 | 6 | try { |
|||
| 828 | 6 | if ($skipCheckCrc) { |
|||
|
0 ignored issues
–
show
|
|||||
| 829 | 6 | while ($offset < $limit) { |
|||
| 830 | $length = min($chunkSize, $limit - $offset); |
||||
| 831 | 6 | $buffer = fread($this->inStream, $length); |
|||
| 832 | |||||
| 833 | if ($buffer === false) { |
||||
| 834 | 6 | throw new ZipException(sprintf('Error reading the contents of entry "%s".', $entry->getName())); |
|||
| 835 | 6 | } |
|||
| 836 | fwrite($outStream, $buffer); |
||||
| 837 | $offset += $length; |
||||
| 838 | 78 | } |
|||
| 839 | } else { |
||||
| 840 | 78 | $contextHash = hash_init('crc32b'); |
|||
| 841 | 76 | ||||
| 842 | 76 | while ($offset < $limit) { |
|||
| 843 | $length = min($chunkSize, $limit - $offset); |
||||
| 844 | 75 | $buffer = fread($this->inStream, $length); |
|||
| 845 | |||||
| 846 | if ($buffer === false) { |
||||
| 847 | 75 | throw new ZipException(sprintf('Error reading the contents of entry "%s".', $entry->getName())); |
|||
| 848 | 75 | } |
|||
| 849 | 75 | fwrite($outStream, $buffer); |
|||
| 850 | hash_update($contextHash, $buffer); |
||||
| 851 | $offset += $length; |
||||
| 852 | 77 | } |
|||
| 853 | |||||
| 854 | 77 | $expectedCrc = (int) hexdec(hash_final($contextHash)); |
|||
| 855 | 1 | ||||
| 856 | if ($expectedCrc !== $entry->getCrc()) { |
||||
| 857 | throw new Crc32Exception($entry->getName(), $expectedCrc, $entry->getCrc()); |
||||
| 858 | 77 | } |
|||
| 859 | 79 | } |
|||
| 860 | 35 | } finally { |
|||
| 861 | for ($i = \count($filters); $i > 0; $i--) { |
||||
| 862 | stream_filter_remove($filters[$i - 1]); |
||||
| 863 | 77 | } |
|||
| 864 | } |
||||
| 865 | } |
||||
| 866 | |||||
| 867 | /** |
||||
| 868 | * @param ZipSourceFileData $zipData |
||||
| 869 | 28 | * @param resource $outStream |
|||
| 870 | */ |
||||
| 871 | 28 | public function copyCompressedDataToStream(ZipSourceFileData $zipData, $outStream) |
|||
| 872 | 28 | { |
|||
| 873 | 28 | if ($zipData->getCompressedSize() > 0) { |
|||
| 874 | fseek($this->inStream, $zipData->getOffset()); |
||||
| 875 | 28 | stream_copy_to_stream($this->inStream, $outStream, $zipData->getCompressedSize()); |
|||
| 876 | } |
||||
| 877 | } |
||||
| 878 | |||||
| 879 | /** |
||||
| 880 | 1 | * @return bool |
|||
| 881 | */ |
||||
| 882 | 1 | protected function isZip64Support() |
|||
| 883 | { |
||||
| 884 | return \PHP_INT_SIZE === 8; // true for 64bit system |
||||
| 885 | 155 | } |
|||
| 886 | |||||
| 887 | 155 | public function close() |
|||
| 888 | 144 | { |
|||
| 889 | if (\is_resource($this->inStream)) { |
||||
| 890 | 155 | fclose($this->inStream); |
|||
| 891 | } |
||||
| 892 | 137 | } |
|||
| 893 | |||||
| 894 | 137 | public function __destruct() |
|||
| 895 | 137 | { |
|||
| 896 | $this->close(); |
||||
| 897 | } |
||||
| 898 | } |
||||
| 899 |
This check looks for parameters that have been defined for a function or method, but which are not used in the method body.