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
![]() |
|||||
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.