ZipReader::copyUncompressedDataToStream()   F
last analyzed

Complexity

Conditions 21
Paths 379

Size

Total Lines 153
Code Lines 92

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 68
CRAP Score 23.602

Importance

Changes 1
Bugs 0 Features 1
Metric Value
cc 21
eloc 92
c 1
b 0
f 1
nc 379
nop 2
dl 0
loc 153
ccs 68
cts 83
cp 0.8193
crap 23.602
rs 1.0958

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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