Total Complexity | 102 |
Total Lines | 862 |
Duplicated Lines | 0 % |
Changes | 3 | ||
Bugs | 1 | Features | 1 |
Complex classes like ZipReader often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
While breaking up the class, it is a good idea to analyze how other classes use ZipReader, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
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 | public function __construct($inStream, array $options = []) |
||
51 | { |
||
52 | if (!\is_resource($inStream)) { |
||
53 | throw new InvalidArgumentException('Stream must be a resource'); |
||
54 | } |
||
55 | $type = get_resource_type($inStream); |
||
56 | |||
57 | if ($type !== 'stream') { |
||
58 | throw new InvalidArgumentException("Invalid resource type {$type}."); |
||
59 | } |
||
60 | $meta = stream_get_meta_data($inStream); |
||
61 | |||
62 | $wrapperType = isset($meta['wrapper_type']) ? $meta['wrapper_type'] : 'Unknown'; |
||
63 | $supportStreamWrapperTypes = ['plainfile', 'PHP', 'user-space']; |
||
64 | |||
65 | if (!\in_array($wrapperType, $supportStreamWrapperTypes, true)) { |
||
66 | throw new InvalidArgumentException( |
||
67 | 'The stream wrapper type "' . $wrapperType . '" is not supported. Support: ' . implode( |
||
68 | ', ', |
||
69 | $supportStreamWrapperTypes |
||
70 | ) |
||
71 | ); |
||
72 | } |
||
73 | |||
74 | if ( |
||
75 | $wrapperType === 'plainfile' && |
||
76 | ( |
||
77 | $meta['stream_type'] === 'dir' || |
||
78 | (isset($meta['uri']) && is_dir($meta['uri'])) |
||
79 | ) |
||
80 | ) { |
||
81 | throw new InvalidArgumentException('Directory stream not supported'); |
||
82 | } |
||
83 | |||
84 | $seekable = $meta['seekable']; |
||
85 | |||
86 | if (!$seekable) { |
||
87 | throw new InvalidArgumentException('Resource does not support seekable.'); |
||
88 | } |
||
89 | $this->size = fstat($inStream)['size']; |
||
90 | $this->inStream = $inStream; |
||
91 | |||
92 | /** @noinspection AdditionOperationOnArraysInspection */ |
||
93 | $options += $this->getDefaultOptions(); |
||
94 | $this->options = $options; |
||
95 | } |
||
96 | |||
97 | /** |
||
98 | * @return array |
||
99 | */ |
||
100 | protected function getDefaultOptions() |
||
101 | { |
||
102 | return [ |
||
103 | ZipOptions::CHARSET => null, |
||
104 | ]; |
||
105 | } |
||
106 | |||
107 | /** |
||
108 | * @throws ZipException |
||
109 | * |
||
110 | * @return ImmutableZipContainer |
||
111 | */ |
||
112 | public function read() |
||
113 | { |
||
114 | if ($this->size < ZipConstants::END_CD_MIN_LEN) { |
||
115 | throw new ZipException('Corrupt zip file'); |
||
116 | } |
||
117 | |||
118 | $endOfCentralDirectory = $this->readEndOfCentralDirectory(); |
||
119 | $entries = $this->readCentralDirectory($endOfCentralDirectory); |
||
120 | |||
121 | return new ImmutableZipContainer($entries, $endOfCentralDirectory->getComment()); |
||
122 | } |
||
123 | |||
124 | /** |
||
125 | * @return array |
||
126 | */ |
||
127 | public function getStreamMetaData() |
||
128 | { |
||
129 | 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 | protected function readEndOfCentralDirectory() |
||
155 | { |
||
156 | if (!$this->findEndOfCentralDirectory()) { |
||
157 | throw new ZipException('Invalid zip file. The end of the central directory could not be found.'); |
||
158 | } |
||
159 | |||
160 | $positionECD = ftell($this->inStream) - 4; |
||
161 | $sizeECD = $this->size - ftell($this->inStream); |
||
162 | $buffer = fread($this->inStream, $sizeECD); |
||
163 | |||
164 | $unpack = unpack( |
||
165 | 'vdiskNo/vcdDiskNo/vcdEntriesDisk/' . |
||
166 | 'vcdEntries/VcdSize/VcdPos/vcommentLength', |
||
167 | substr($buffer, 0, 18) |
||
168 | ); |
||
169 | |||
170 | if ( |
||
171 | $unpack['diskNo'] !== 0 || |
||
172 | $unpack['cdDiskNo'] !== 0 || |
||
173 | $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 | $comment = null; |
||
181 | |||
182 | if ($unpack['commentLength'] > 0) { |
||
183 | $comment = substr($buffer, 18, $unpack['commentLength']); |
||
184 | } |
||
185 | |||
186 | // Check for ZIP64 End Of Central Directory Locator exists. |
||
187 | $zip64ECDLocatorPosition = $positionECD - ZipConstants::ZIP64_END_CD_LOC_LEN; |
||
188 | fseek($this->inStream, $zip64ECDLocatorPosition); |
||
189 | // zip64 end of central dir locator |
||
190 | // signature 4 bytes (0x07064b50) |
||
191 | if ($zip64ECDLocatorPosition > 0 && unpack( |
||
192 | 'V', |
||
193 | fread($this->inStream, 4) |
||
194 | )[1] === ZipConstants::ZIP64_END_CD_LOC) { |
||
195 | if (!$this->isZip64Support()) { |
||
196 | throw new ZipException('ZIP64 not supported this archive.'); |
||
197 | } |
||
198 | |||
199 | $positionECD = $this->findZip64ECDPosition(); |
||
200 | $endCentralDirectory = $this->readZip64EndOfCentralDirectory($positionECD); |
||
201 | $endCentralDirectory->setComment($comment); |
||
202 | } else { |
||
203 | $endCentralDirectory = new EndOfCentralDirectory( |
||
204 | $unpack['cdEntries'], |
||
205 | $unpack['cdPos'], |
||
206 | $unpack['cdSize'], |
||
207 | false, |
||
208 | $comment |
||
209 | ); |
||
210 | } |
||
211 | |||
212 | return $endCentralDirectory; |
||
213 | } |
||
214 | |||
215 | /** |
||
216 | * @return bool |
||
217 | */ |
||
218 | protected function findEndOfCentralDirectory() |
||
219 | { |
||
220 | $max = $this->size - ZipConstants::END_CD_MIN_LEN; |
||
221 | $min = $max >= 0xffff ? $max - 0xffff : 0; |
||
222 | // Search for End of central directory record. |
||
223 | for ($position = $max; $position >= $min; $position--) { |
||
224 | fseek($this->inStream, $position); |
||
225 | // end of central dir signature 4 bytes (0x06054b50) |
||
226 | if (unpack('V', fread($this->inStream, 4))[1] !== ZipConstants::END_CD) { |
||
227 | continue; |
||
228 | } |
||
229 | |||
230 | return true; |
||
231 | } |
||
232 | |||
233 | 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 | protected function findZip64ECDPosition() |
||
252 | { |
||
253 | $diskNo = unpack('V', fread($this->inStream, 4))[1]; |
||
254 | $zip64ECDPos = PackUtil::unpackLongLE(fread($this->inStream, 8)); |
||
255 | $totalDisks = unpack('V', fread($this->inStream, 4))[1]; |
||
256 | |||
257 | if ($diskNo !== 0 || $totalDisks > 1) { |
||
258 | throw new ZipException('ZIP file spanning/splitting is not supported!'); |
||
259 | } |
||
260 | |||
261 | 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 | protected function readZip64EndOfCentralDirectory($zip64ECDPosition) |
||
294 | { |
||
295 | fseek($this->inStream, $zip64ECDPosition); |
||
296 | |||
297 | $buffer = fread($this->inStream, ZipConstants::ZIP64_END_OF_CD_LEN); |
||
298 | |||
299 | if (unpack('V', $buffer)[1] !== ZipConstants::ZIP64_END_CD) { |
||
300 | throw new ZipException('Expected ZIP64 End Of Central Directory Record!'); |
||
301 | } |
||
302 | |||
303 | $data = unpack( |
||
304 | // 'Psize/vversionMadeBy/vextractVersion/' . |
||
305 | 'VdiskNo/VcdDiskNo', |
||
306 | substr($buffer, 16, 8) |
||
307 | ); |
||
308 | |||
309 | $cdEntriesDisk = PackUtil::unpackLongLE(substr($buffer, 24, 8)); |
||
310 | $entryCount = PackUtil::unpackLongLE(substr($buffer, 32, 8)); |
||
311 | $cdSize = PackUtil::unpackLongLE(substr($buffer, 40, 8)); |
||
312 | $cdPos = PackUtil::unpackLongLE(substr($buffer, 48, 8)); |
||
313 | |||
314 | // $platform = ZipPlatform::fromValue(($data['versionMadeBy'] & 0xFF00) >> 8); |
||
315 | // $softwareVersion = $data['versionMadeBy'] & 0x00FF; |
||
316 | |||
317 | if ($data['diskNo'] !== 0 || $data['cdDiskNo'] !== 0 || $entryCount !== $cdEntriesDisk) { |
||
318 | throw new ZipException('ZIP file spanning/splitting is not supported!'); |
||
319 | } |
||
320 | |||
321 | 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 | return new EndOfCentralDirectory( |
||
328 | $entryCount, |
||
329 | $cdPos, |
||
330 | $cdSize, |
||
331 | 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 | protected function readCentralDirectory(EndOfCentralDirectory $endCD) |
||
350 | { |
||
351 | $entries = []; |
||
352 | |||
353 | $cdOffset = $endCD->getCdOffset(); |
||
354 | fseek($this->inStream, $cdOffset); |
||
355 | |||
356 | if (!($cdStream = fopen('php://temp', 'w+b'))) { |
||
357 | throw new ZipException('Temp resource can not open from write'); |
||
358 | } |
||
359 | stream_copy_to_stream($this->inStream, $cdStream, $endCD->getCdSize()); |
||
360 | rewind($cdStream); |
||
361 | for ($numEntries = $endCD->getEntryCount(); $numEntries > 0; $numEntries--) { |
||
362 | $zipEntry = $this->readZipEntry($cdStream); |
||
363 | |||
364 | $entryName = $zipEntry->getName(); |
||
365 | |||
366 | /** @var UnicodePathExtraField|null $unicodePathExtraField */ |
||
367 | $unicodePathExtraField = $zipEntry->getExtraField(UnicodePathExtraField::HEADER_ID); |
||
368 | |||
369 | if ($unicodePathExtraField !== null) { |
||
370 | $unicodePath = $unicodePathExtraField->getUnicodeValue(); |
||
371 | |||
372 | if ($unicodePath !== null) { |
||
373 | $unicodePath = str_replace('\\', '/', $unicodePath); |
||
374 | |||
375 | if ( |
||
376 | $unicodePath !== '' && |
||
377 | substr_count($entryName, '/') === substr_count($unicodePath, '/') |
||
378 | ) { |
||
379 | $entryName = $unicodePath; |
||
380 | } |
||
381 | } |
||
382 | } |
||
383 | |||
384 | $entries[$entryName] = $zipEntry; |
||
385 | } |
||
386 | |||
387 | return $entries; |
||
388 | } |
||
389 | |||
390 | /** |
||
391 | * Read central directory entry. |
||
392 | * |
||
393 | * central file header signature 4 bytes (0x02014b50) |
||
394 | * version made by 2 bytes |
||
395 | * version needed to extract 2 bytes |
||
396 | * general purpose bit flag 2 bytes |
||
397 | * compression method 2 bytes |
||
398 | * last mod file time 2 bytes |
||
399 | * last mod file date 2 bytes |
||
400 | * crc-32 4 bytes |
||
401 | * compressed size 4 bytes |
||
402 | * uncompressed size 4 bytes |
||
403 | * file name length 2 bytes |
||
404 | * extra field length 2 bytes |
||
405 | * file comment length 2 bytes |
||
406 | * disk number start 2 bytes |
||
407 | * internal file attributes 2 bytes |
||
408 | * external file attributes 4 bytes |
||
409 | * relative offset of local header 4 bytes |
||
410 | * |
||
411 | * file name (variable size) |
||
412 | * extra field (variable size) |
||
413 | * file comment (variable size) |
||
414 | * |
||
415 | * @param resource $stream |
||
416 | * |
||
417 | * @throws ZipException |
||
418 | * |
||
419 | * @return ZipEntry |
||
420 | */ |
||
421 | protected function readZipEntry($stream) |
||
513 | } |
||
514 | |||
515 | /** |
||
516 | * @param string $buffer |
||
517 | * @param ZipEntry $zipEntry |
||
518 | * @param bool $local |
||
519 | * |
||
520 | * @return ExtraFieldsCollection |
||
521 | */ |
||
522 | protected function parseExtraFields($buffer, ZipEntry $zipEntry, $local = false) |
||
523 | { |
||
524 | $collection = $local ? |
||
525 | $zipEntry->getLocalExtraFields() : |
||
526 | $zipEntry->getCdExtraFields(); |
||
527 | |||
528 | if (!empty($buffer)) { |
||
529 | $pos = 0; |
||
530 | $endPos = \strlen($buffer); |
||
531 | |||
532 | while ($endPos - $pos >= 4) { |
||
533 | /** @var int[] $data */ |
||
534 | $data = unpack('vheaderId/vdataSize', substr($buffer, $pos, 4)); |
||
535 | $pos += 4; |
||
536 | |||
537 | if ($endPos - $pos - $data['dataSize'] < 0) { |
||
538 | break; |
||
539 | } |
||
540 | $bufferData = substr($buffer, $pos, $data['dataSize']); |
||
541 | $headerId = $data['headerId']; |
||
542 | |||
543 | /** @var string|ZipExtraField|null $className */ |
||
544 | $className = ZipExtraDriver::getClassNameOrNull($headerId); |
||
545 | |||
546 | if ($className !== null) { |
||
547 | try { |
||
548 | $extraField = $local ? |
||
549 | \call_user_func([$className, 'unpackLocalFileData'], $bufferData, $zipEntry) : |
||
550 | \call_user_func([$className, 'unpackCentralDirData'], $bufferData, $zipEntry); |
||
551 | } catch (\Throwable $e) { |
||
552 | throw new \RuntimeException( |
||
553 | sprintf( |
||
554 | 'Error parse %s extra field 0x%04X', |
||
555 | $local ? 'local' : 'central directory', |
||
556 | $headerId |
||
557 | ) |
||
558 | ); |
||
559 | } |
||
560 | } else { |
||
561 | $extraField = new UnrecognizedExtraField($headerId, $bufferData); |
||
562 | } |
||
563 | $collection->add($extraField); |
||
564 | $pos += $data['dataSize']; |
||
565 | } |
||
566 | } |
||
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 | * @throws ZipException |
||
614 | */ |
||
615 | protected function loadLocalExtraFields(ZipEntry $entry) |
||
643 | } |
||
644 | |||
645 | /** |
||
646 | * @param ZipEntry $zipEntry |
||
647 | * |
||
648 | * @throws ZipException |
||
649 | */ |
||
650 | private function handleExtraEncryptionFields(ZipEntry $zipEntry) |
||
651 | { |
||
652 | if ($zipEntry->isEncrypted()) { |
||
653 | if ($zipEntry->getCompressionMethod() === ZipCompressionMethod::WINZIP_AES) { |
||
654 | /** @var WinZipAesExtraField|null $extraField */ |
||
655 | $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 | ); |
||
665 | } |
||
666 | $zipEntry->setCompressionMethod($extraField->getCompressionMethod()); |
||
667 | $zipEntry->setEncryptionMethod($extraField->getEncryptionMethod()); |
||
668 | } else { |
||
669 | $zipEntry->setEncryptionMethod(ZipEncryptionMethod::PKWARE); |
||
670 | } |
||
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 | * @param ZipEntry $zipEntry |
||
681 | */ |
||
682 | protected function handleExtraFields(ZipEntry $zipEntry) |
||
|
|||
683 | { |
||
684 | } |
||
685 | |||
686 | /** |
||
687 | * @param ZipSourceFileData $zipFileData |
||
688 | * |
||
689 | * @throws ZipException |
||
690 | * @throws Crc32Exception |
||
691 | * |
||
692 | * @return resource |
||
693 | */ |
||
694 | public function getEntryStream(ZipSourceFileData $zipFileData) |
||
695 | { |
||
696 | $outStream = fopen('php://temp', 'w+b'); |
||
697 | $this->copyUncompressedDataToStream($zipFileData, $outStream); |
||
698 | rewind($outStream); |
||
699 | |||
700 | return $outStream; |
||
701 | } |
||
702 | |||
703 | /** |
||
704 | * @param ZipSourceFileData $zipFileData |
||
705 | * @param resource $outStream |
||
706 | * |
||
707 | * @throws Crc32Exception |
||
708 | * @throws ZipException |
||
709 | */ |
||
710 | public function copyUncompressedDataToStream(ZipSourceFileData $zipFileData, $outStream) |
||
711 | { |
||
712 | if (!\is_resource($outStream)) { |
||
713 | throw new InvalidArgumentException('outStream is not resource'); |
||
714 | } |
||
715 | |||
716 | $entry = $zipFileData->getSourceEntry(); |
||
717 | |||
718 | // if ($entry->isDirectory()) { |
||
719 | // throw new InvalidArgumentException('Streams not supported for directories'); |
||
720 | // } |
||
721 | |||
722 | if ($entry->isStrongEncryption()) { |
||
723 | throw new ZipException('Not support encryption zip.'); |
||
724 | } |
||
725 | |||
726 | $compressionMethod = $entry->getCompressionMethod(); |
||
727 | |||
728 | fseek($this->inStream, $zipFileData->getOffset()); |
||
729 | |||
730 | $filters = []; |
||
731 | |||
732 | $skipCheckCrc = false; |
||
733 | $isEncrypted = $entry->isEncrypted(); |
||
734 | |||
735 | if ($isEncrypted) { |
||
736 | if ($entry->getPassword() === null) { |
||
737 | throw new ZipException('Can not password from entry ' . $entry->getName()); |
||
738 | } |
||
739 | |||
740 | if (ZipEncryptionMethod::isWinZipAesMethod($entry->getEncryptionMethod())) { |
||
741 | /** @var WinZipAesExtraField|null $winZipAesExtra */ |
||
742 | $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 | ); |
||
748 | } |
||
749 | $compressionMethod = $winZipAesExtra->getCompressionMethod(); |
||
750 | |||
751 | WinZipAesDecryptionStreamFilter::register(); |
||
752 | $cipherFilterName = WinZipAesDecryptionStreamFilter::FILTER_NAME; |
||
753 | |||
754 | if ($winZipAesExtra->isV2()) { |
||
755 | $skipCheckCrc = true; |
||
756 | } |
||
757 | } else { |
||
758 | PKDecryptionStreamFilter::register(); |
||
759 | $cipherFilterName = PKDecryptionStreamFilter::FILTER_NAME; |
||
760 | } |
||
761 | $encContextFilter = stream_filter_append( |
||
762 | $this->inStream, |
||
763 | $cipherFilterName, |
||
764 | \STREAM_FILTER_READ, |
||
765 | [ |
||
766 | 'entry' => $entry, |
||
767 | ] |
||
768 | ); |
||
769 | |||
770 | if (!$encContextFilter) { |
||
771 | throw new \RuntimeException('Not apply filter ' . $cipherFilterName); |
||
772 | } |
||
773 | $filters[] = $encContextFilter; |
||
774 | } |
||
775 | |||
776 | // hack, see https://groups.google.com/forum/#!topic/alt.comp.lang.php/37_JZeW63uc |
||
777 | $pos = ftell($this->inStream); |
||
778 | rewind($this->inStream); |
||
779 | fseek($this->inStream, $pos); |
||
780 | |||
781 | $contextDecompress = null; |
||
782 | switch ($compressionMethod) { |
||
783 | case ZipCompressionMethod::STORED: |
||
784 | // file without compression, do nothing |
||
785 | break; |
||
786 | |||
787 | case ZipCompressionMethod::DEFLATED: |
||
788 | if (!($contextDecompress = stream_filter_append( |
||
789 | $this->inStream, |
||
790 | 'zlib.inflate', |
||
791 | \STREAM_FILTER_READ |
||
792 | ))) { |
||
793 | throw new \RuntimeException('Could not append filter "zlib.inflate" to stream'); |
||
794 | } |
||
795 | $filters[] = $contextDecompress; |
||
796 | |||
797 | break; |
||
798 | |||
799 | case ZipCompressionMethod::BZIP2: |
||
800 | if (!($contextDecompress = stream_filter_append( |
||
801 | $this->inStream, |
||
802 | 'bzip2.decompress', |
||
803 | \STREAM_FILTER_READ |
||
804 | ))) { |
||
805 | throw new \RuntimeException('Could not append filter "bzip2.decompress" to stream'); |
||
806 | } |
||
807 | $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 | } |
||
821 | |||
822 | $limit = $zipFileData->getUncompressedSize(); |
||
823 | |||
824 | $offset = 0; |
||
825 | $chunkSize = 8192; |
||
826 | |||
827 | try { |
||
828 | if ($skipCheckCrc) { |
||
829 | while ($offset < $limit) { |
||
830 | $length = min($chunkSize, $limit - $offset); |
||
831 | $buffer = fread($this->inStream, $length); |
||
832 | |||
833 | if ($buffer === false) { |
||
834 | throw new ZipException(sprintf('Error reading the contents of entry "%s".', $entry->getName())); |
||
835 | } |
||
836 | fwrite($outStream, $buffer); |
||
837 | $offset += $length; |
||
838 | } |
||
839 | } else { |
||
840 | $contextHash = hash_init('crc32b'); |
||
841 | |||
842 | while ($offset < $limit) { |
||
843 | $length = min($chunkSize, $limit - $offset); |
||
844 | $buffer = fread($this->inStream, $length); |
||
845 | |||
846 | if ($buffer === false) { |
||
847 | throw new ZipException(sprintf('Error reading the contents of entry "%s".', $entry->getName())); |
||
848 | } |
||
849 | fwrite($outStream, $buffer); |
||
850 | hash_update($contextHash, $buffer); |
||
851 | $offset += $length; |
||
852 | } |
||
853 | |||
854 | $expectedCrc = (int) hexdec(hash_final($contextHash)); |
||
855 | |||
856 | if ($expectedCrc !== $entry->getCrc()) { |
||
857 | throw new Crc32Exception($entry->getName(), $expectedCrc, $entry->getCrc()); |
||
858 | } |
||
859 | } |
||
860 | } finally { |
||
861 | for ($i = \count($filters); $i > 0; $i--) { |
||
862 | stream_filter_remove($filters[$i - 1]); |
||
863 | } |
||
864 | } |
||
865 | } |
||
866 | |||
867 | /** |
||
868 | * @param ZipSourceFileData $zipData |
||
869 | * @param resource $outStream |
||
870 | */ |
||
871 | public function copyCompressedDataToStream(ZipSourceFileData $zipData, $outStream) |
||
872 | { |
||
873 | if ($zipData->getCompressedSize() > 0) { |
||
874 | fseek($this->inStream, $zipData->getOffset()); |
||
875 | stream_copy_to_stream($this->inStream, $outStream, $zipData->getCompressedSize()); |
||
876 | } |
||
877 | } |
||
878 | |||
879 | /** |
||
880 | * @return bool |
||
881 | */ |
||
882 | protected function isZip64Support() |
||
883 | { |
||
884 | return \PHP_INT_SIZE === 8; // true for 64bit system |
||
885 | } |
||
886 | |||
887 | public function close() |
||
888 | { |
||
889 | if (\is_resource($this->inStream)) { |
||
890 | fclose($this->inStream); |
||
891 | } |
||
892 | } |
||
893 | |||
894 | public function __destruct() |
||
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.