ZipReader   F
last analyzed

Complexity

Total Complexity 102

Size/Duplication

Total Lines 862
Duplicated Lines 0 %

Test Coverage

Coverage 84.46%

Importance

Changes 3
Bugs 1 Features 1
Metric Value
eloc 358
c 3
b 1
f 1
dl 0
loc 862
ccs 299
cts 354
cp 0.8446
rs 2
wmc 102

21 Methods

Rating   Name   Duplication   Size   Complexity  
A loadLocalExtraFields() 0 28 3
A __destruct() 0 3 1
A read() 0 10 2
B readZip64EndOfCentralDirectory() 0 39 7
A handleZip64Extra() 0 16 4
A close() 0 4 2
B __construct() 0 45 10
A findEndOfCentralDirectory() 0 16 4
A findZip64ECDPosition() 0 11 3
B readEndOfCentralDirectory() 0 59 9
A copyCompressedDataToStream() 0 5 2
A getStreamMetaData() 0 3 1
A getDefaultOptions() 0 4 1
A handleExtraFields() 0 2 1
B readZipEntry() 0 92 9
A getEntryStream() 0 7 1
B parseExtraFields() 0 45 8
B readCentralDirectory() 0 41 8
F copyUncompressedDataToStream() 0 153 21
A isZip64Support() 0 3 1
A handleExtraEncryptionFields() 0 20 4

How to fix   Complexity   

Complex Class

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
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
Unused Code introduced by
The parameter $zipEntry is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

682
    protected function handleExtraFields(/** @scrutinizer ignore-unused */ ZipEntry $zipEntry)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
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
Bug introduced by
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 ignore-type  annotation

697
        $this->copyUncompressedDataToStream($zipFileData, /** @scrutinizer ignore-type */ $outStream);
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
introduced by
$encContextFilter is of type resource, thus it always evaluated to false.
Loading history...
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
introduced by
The condition $skipCheckCrc is always false.
Loading history...
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