Passed
Push — master ( d305ab...074443 )
by Alexey
03:50 queued 12s
created

ZipReader::parseExtraFields()   B

Complexity

Conditions 8
Paths 6

Size

Total Lines 45
Code Lines 28

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 25
CRAP Score 8.0036

Importance

Changes 1
Bugs 0 Features 1
Metric Value
cc 8
eloc 28
c 1
b 0
f 1
nc 6
nop 3
dl 0
loc 45
ccs 25
cts 26
cp 0.9615
crap 8.0036
rs 8.4444
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 98
    public function __construct($inStream, array $options = [])
51
    {
52 98
        if (!\is_resource($inStream)) {
53 2
            throw new InvalidArgumentException('Stream must be a resource');
54
        }
55 96
        $type = get_resource_type($inStream);
56
57 96
        if ($type !== 'stream') {
58 1
            throw new InvalidArgumentException("Invalid resource type {$type}.");
59
        }
60 95
        $meta = stream_get_meta_data($inStream);
61
62 95
        $wrapperType = isset($meta['wrapper_type']) ? $meta['wrapper_type'] : 'Unknown';
63 95
        $supportStreamWrapperTypes = ['plainfile', 'PHP', 'user-space'];
64
65 95
        if (!\in_array($wrapperType, $supportStreamWrapperTypes, true)) {
66 2
            throw new InvalidArgumentException(
67 2
                'The stream wrapper type "' . $wrapperType . '" is not supported. Support: ' . implode(
68 2
                    ', ',
69
                    $supportStreamWrapperTypes
70
                )
71
            );
72
        }
73
74
        if (
75 93
            $wrapperType === 'plainfile' &&
76
            (
77 84
                $meta['stream_type'] === 'dir' ||
78 93
                (isset($meta['uri']) && is_dir($meta['uri']))
79
            )
80
        ) {
81 2
            throw new InvalidArgumentException('Directory stream not supported');
82
        }
83
84 91
        $seekable = $meta['seekable'];
85
86 91
        if (!$seekable) {
87
            throw new InvalidArgumentException('Resource does not support seekable.');
88
        }
89 91
        $this->size = fstat($inStream)['size'];
90 91
        $this->inStream = $inStream;
91
92
        /** @noinspection AdditionOperationOnArraysInspection */
93 91
        $options += $this->getDefaultOptions();
94 91
        $this->options = $options;
95 91
    }
96
97
    /**
98
     * @return array
99
     */
100 91
    protected function getDefaultOptions()
101
    {
102
        return [
103 91
            ZipOptions::CHARSET => null,
104
        ];
105
    }
106
107
    /**
108
     * @throws ZipException
109
     *
110
     * @return ImmutableZipContainer
111
     */
112 91
    public function read()
113
    {
114 91
        if ($this->size < ZipConstants::END_CD_MIN_LEN) {
115 2
            throw new ZipException('Corrupt zip file');
116
        }
117
118 89
        $endOfCentralDirectory = $this->readEndOfCentralDirectory();
119 86
        $entries = $this->readCentralDirectory($endOfCentralDirectory);
120
121 85
        return new ImmutableZipContainer($entries, $endOfCentralDirectory->getComment());
122
    }
123
124
    /**
125
     * @return array
126
     */
127 5
    public function getStreamMetaData()
128
    {
129 5
        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 89
    protected function readEndOfCentralDirectory()
155
    {
156 89
        if (!$this->findEndOfCentralDirectory()) {
157 3
            throw new ZipException('Invalid zip file. The end of the central directory could not be found.');
158
        }
159
160 86
        $positionECD = ftell($this->inStream) - 4;
161 86
        $sizeECD = $this->size - ftell($this->inStream);
162 86
        $buffer = fread($this->inStream, $sizeECD);
163
164 86
        $unpack = unpack(
165
            'vdiskNo/vcdDiskNo/vcdEntriesDisk/' .
166 86
            'vcdEntries/VcdSize/VcdPos/vcommentLength',
167 86
            substr($buffer, 0, 18)
168
        );
169
170
        if (
171 86
            $unpack['diskNo'] !== 0 ||
172 86
            $unpack['cdDiskNo'] !== 0 ||
173 86
            $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 86
        $comment = null;
181
182 86
        if ($unpack['commentLength'] > 0) {
183 3
            $comment = substr($buffer, 18, $unpack['commentLength']);
184
        }
185
186
        // Check for ZIP64 End Of Central Directory Locator exists.
187 86
        $zip64ECDLocatorPosition = $positionECD - ZipConstants::ZIP64_END_CD_LOC_LEN;
188 86
        fseek($this->inStream, $zip64ECDLocatorPosition);
189
        // zip64 end of central dir locator
190
        // signature                       4 bytes  (0x07064b50)
191 86
        if ($zip64ECDLocatorPosition > 0 && unpack(
192 85
            'V',
193 85
            fread($this->inStream, 4)
194 86
        )[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 85
            $endCentralDirectory = new EndOfCentralDirectory(
204 85
                $unpack['cdEntries'],
205 85
                $unpack['cdPos'],
206 85
                $unpack['cdSize'],
207 85
                false,
208
                $comment
209
            );
210
        }
211
212 86
        return $endCentralDirectory;
213
    }
214
215
    /**
216
     * @return bool
217
     */
218 89
    protected function findEndOfCentralDirectory()
219
    {
220 89
        $max = $this->size - ZipConstants::END_CD_MIN_LEN;
221 89
        $min = $max >= 0xffff ? $max - 0xffff : 0;
222
        // Search for End of central directory record.
223 89
        for ($position = $max; $position >= $min; $position--) {
224 89
            fseek($this->inStream, $position);
225
            // end of central dir signature    4 bytes  (0x06054b50)
226 89
            if (unpack('V', fread($this->inStream, 4))[1] !== ZipConstants::END_CD) {
227 6
                continue;
228
            }
229
230 86
            return true;
231
        }
232
233 3
        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 86
    protected function readCentralDirectory(EndOfCentralDirectory $endCD)
350
    {
351 86
        $entries = [];
352
353 86
        $cdOffset = $endCD->getCdOffset();
354 86
        fseek($this->inStream, $cdOffset);
355
356 86
        if (!($cdStream = fopen('php://temp', 'w+b'))) {
357
            throw new ZipException('Temp resource can not open from write');
358
        }
359 86
        stream_copy_to_stream($this->inStream, $cdStream, $endCD->getCdSize());
360 86
        rewind($cdStream);
361 86
        for ($numEntries = $endCD->getEntryCount(); $numEntries > 0; $numEntries--) {
362 85
            $zipEntry = $this->readZipEntry($cdStream);
363
364 84
            $entryName = $zipEntry->getName();
365
366
            /** @var UnicodePathExtraField|null $unicodePathExtraField */
367 84
            $unicodePathExtraField = $zipEntry->getExtraField(UnicodePathExtraField::HEADER_ID);
368
369 84
            if ($unicodePathExtraField !== null && $unicodePathExtraField->getCrc32() === crc32($entryName)) {
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 84
            $entries[$entryName] = $zipEntry;
385
        }
386
387 85
        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 85
    protected function readZipEntry($stream)
422
    {
423 85
        if (unpack('V', fread($stream, 4))[1] !== ZipConstants::CENTRAL_FILE_HEADER) {
424 1
            throw new ZipException('Corrupt zip file. Cannot read zip entry.');
425
        }
426
427 84
        $unpack = unpack(
428
            'vversionMadeBy/vversionNeededToExtract/' .
429
            'vgeneralPurposeBitFlag/vcompressionMethod/' .
430
            'VlastModFile/Vcrc/VcompressedSize/' .
431
            'VuncompressedSize/vfileNameLength/vextraFieldLength/' .
432
            'vfileCommentLength/vdiskNumberStart/vinternalFileAttributes/' .
433 84
            'VexternalFileAttributes/VoffsetLocalHeader',
434 84
            fread($stream, 42)
435
        );
436
437 84
        if ($unpack['diskNumberStart'] !== 0) {
438
            throw new ZipException('ZIP file spanning/splitting is not supported!');
439
        }
440
441 84
        $generalPurposeBitFlags = $unpack['generalPurposeBitFlag'];
442 84
        $isUtf8 = ($generalPurposeBitFlags & GeneralPurposeBitFlag::UTF8) !== 0;
443
444 84
        $name = fread($stream, $unpack['fileNameLength']);
445
446 84
        $createdOS = ($unpack['versionMadeBy'] & 0xFF00) >> 8;
447 84
        $softwareVersion = $unpack['versionMadeBy'] & 0x00FF;
448
449 84
        $extractedOS = ($unpack['versionNeededToExtract'] & 0xFF00) >> 8;
450 84
        $extractVersion = $unpack['versionNeededToExtract'] & 0x00FF;
451
452 84
        $dosTime = $unpack['lastModFile'];
453
454 84
        $comment = null;
455
456 84
        if ($unpack['fileCommentLength'] > 0) {
457 2
            $comment = fread($stream, $unpack['fileCommentLength']);
458
        }
459
460
        // decode code page names
461 84
        $fallbackCharset = null;
462
463 84
        if (!$isUtf8 && isset($this->options[ZipOptions::CHARSET])) {
464
            $charset = $this->options[ZipOptions::CHARSET];
465
466
            $fallbackCharset = $charset;
467
            $name = DosCodePage::toUTF8($name, $charset);
468
469
            if ($comment !== null) {
470
                $comment = DosCodePage::toUTF8($comment, $charset);
471
            }
472
        }
473
474 84
        $zipEntry = ZipEntry::create(
475 84
            $name,
476 84
            $createdOS,
477 84
            $extractedOS,
478 84
            $softwareVersion,
479 84
            $extractVersion,
480 84
            $unpack['compressionMethod'],
481 84
            $generalPurposeBitFlags,
482 84
            $dosTime,
483 84
            $unpack['crc'],
484 84
            $unpack['compressedSize'],
485 84
            $unpack['uncompressedSize'],
486 84
            $unpack['internalFileAttributes'],
487 84
            $unpack['externalFileAttributes'],
488 84
            $unpack['offsetLocalHeader'],
489 84
            $comment,
490 84
            $fallbackCharset
491
        );
492
493 84
        if ($unpack['extraFieldLength'] > 0) {
494 17
            $this->parseExtraFields(
495 17
                fread($stream, $unpack['extraFieldLength']),
496
                $zipEntry,
497 17
                false
498
            );
499
500
            /** @var Zip64ExtraField|null $extraZip64 */
501 17
            $extraZip64 = $zipEntry->getCdExtraField(Zip64ExtraField::HEADER_ID);
502
503 17
            if ($extraZip64 !== null) {
504
                $this->handleZip64Extra($extraZip64, $zipEntry);
505
            }
506
        }
507
508 84
        $this->loadLocalExtraFields($zipEntry);
509 84
        $this->handleExtraEncryptionFields($zipEntry);
510 84
        $this->handleExtraFields($zipEntry);
511
512 84
        return $zipEntry;
513
    }
514
515
    /**
516
     * @param string   $buffer
517
     * @param ZipEntry $zipEntry
518
     * @param bool     $local
519
     *
520
     * @return ExtraFieldsCollection
521
     */
522 17
    protected function parseExtraFields($buffer, ZipEntry $zipEntry, $local = false)
523
    {
524 17
        $collection = $local ?
525 16
            $zipEntry->getLocalExtraFields() :
526 17
            $zipEntry->getCdExtraFields();
527
528 17
        if (!empty($buffer)) {
529 17
            $pos = 0;
530 17
            $endPos = \strlen($buffer);
531
532 17
            while ($endPos - $pos >= 4) {
533
                /** @var int[] $data */
534 17
                $data = unpack('vheaderId/vdataSize', substr($buffer, $pos, 4));
535 17
                $pos += 4;
536
537 17
                if ($endPos - $pos - $data['dataSize'] < 0) {
538 1
                    break;
539
                }
540 17
                $bufferData = substr($buffer, $pos, $data['dataSize']);
541 17
                $headerId = $data['headerId'];
542
543
                /** @var string|ZipExtraField|null $className */
544 17
                $className = ZipExtraDriver::getClassNameOrNull($headerId);
545
546
                try {
547 17
                    if ($className !== null) {
548
                        try {
549 17
                            $extraField = $local ?
550 16
                                \call_user_func([$className, 'unpackLocalFileData'], $bufferData, $zipEntry) :
551 17
                                \call_user_func([$className, 'unpackCentralDirData'], $bufferData, $zipEntry);
552
                        } catch (\Throwable $e) {
553
                            // skip errors while parsing invalid data
554 17
                            continue;
555
                        }
556
                    } else {
557 2
                        $extraField = new UnrecognizedExtraField($headerId, $bufferData);
558
                    }
559 17
                    $collection->add($extraField);
560 17
                } finally {
561 17
                    $pos += $data['dataSize'];
562
                }
563
            }
564
        }
565
566 17
        return $collection;
567
    }
568
569
    /**
570
     * @param Zip64ExtraField $extraZip64
571
     * @param ZipEntry        $zipEntry
572
     */
573
    protected function handleZip64Extra(Zip64ExtraField $extraZip64, ZipEntry $zipEntry)
574
    {
575
        $uncompressedSize = $extraZip64->getUncompressedSize();
576
        $compressedSize = $extraZip64->getCompressedSize();
577
        $localHeaderOffset = $extraZip64->getLocalHeaderOffset();
578
579
        if ($uncompressedSize !== null) {
580
            $zipEntry->setUncompressedSize($uncompressedSize);
581
        }
582
583
        if ($compressedSize !== null) {
584
            $zipEntry->setCompressedSize($compressedSize);
585
        }
586
587
        if ($localHeaderOffset !== null) {
588
            $zipEntry->setLocalHeaderOffset($localHeaderOffset);
589
        }
590
    }
591
592
    /**
593
     * Read Local File Header.
594
     *
595
     * local file header signature     4 bytes  (0x04034b50)
596
     * version needed to extract       2 bytes
597
     * general purpose bit flag        2 bytes
598
     * compression method              2 bytes
599
     * last mod file time              2 bytes
600
     * last mod file date              2 bytes
601
     * crc-32                          4 bytes
602
     * compressed size                 4 bytes
603
     * uncompressed size               4 bytes
604
     * file name length                2 bytes
605
     * extra field length              2 bytes
606
     * file name (variable size)
607
     * extra field (variable size)
608
     *
609
     * @param ZipEntry $entry
610
     *
611
     * @throws ZipException
612
     */
613 84
    protected function loadLocalExtraFields(ZipEntry $entry)
614
    {
615 84
        $offsetLocalHeader = $entry->getLocalHeaderOffset();
616
617 84
        fseek($this->inStream, $offsetLocalHeader);
618
619 84
        if (unpack('V', fread($this->inStream, 4))[1] !== ZipConstants::LOCAL_FILE_HEADER) {
620
            throw new ZipException(sprintf('%s (expected Local File Header)', $entry->getName()));
621
        }
622
623 84
        fseek($this->inStream, $offsetLocalHeader + ZipConstants::LFH_FILENAME_LENGTH_POS);
624 84
        $unpack = unpack('vfileNameLength/vextraFieldLength', fread($this->inStream, 4));
625 84
        $offsetData = ftell($this->inStream)
626 84
            + $unpack['fileNameLength']
627 84
            + $unpack['extraFieldLength'];
628
629 84
        fseek($this->inStream, $unpack['fileNameLength'], \SEEK_CUR);
630
631 84
        if ($unpack['extraFieldLength'] > 0) {
632 16
            $this->parseExtraFields(
633 16
                fread($this->inStream, $unpack['extraFieldLength']),
634
                $entry,
635 16
                true
636
            );
637
        }
638
639 84
        $zipData = new ZipSourceFileData($this, $entry, $offsetData);
640 84
        $entry->setData($zipData);
641 84
    }
642
643
    /**
644
     * @param ZipEntry $zipEntry
645
     *
646
     * @throws ZipException
647
     */
648 84
    private function handleExtraEncryptionFields(ZipEntry $zipEntry)
649
    {
650 84
        if ($zipEntry->isEncrypted()) {
651 11
            if ($zipEntry->getCompressionMethod() === ZipCompressionMethod::WINZIP_AES) {
652
                /** @var WinZipAesExtraField|null $extraField */
653 9
                $extraField = $zipEntry->getExtraField(WinZipAesExtraField::HEADER_ID);
654
655 9
                if ($extraField === null) {
656
                    throw new ZipException(
657
                        sprintf(
658
                            'Extra field 0x%04x (WinZip-AES Encryption) expected for compression method %d',
659
                            WinZipAesExtraField::HEADER_ID,
660
                            $zipEntry->getCompressionMethod()
661
                        )
662
                    );
663
                }
664 9
                $zipEntry->setCompressionMethod($extraField->getCompressionMethod());
665 9
                $zipEntry->setEncryptionMethod($extraField->getEncryptionMethod());
666
            } else {
667 5
                $zipEntry->setEncryptionMethod(ZipEncryptionMethod::PKWARE);
668
            }
669
        }
670 84
    }
671
672
    /**
673
     * Handle extra data in zip records.
674
     *
675
     * This is a special method in which you can process ExtraField
676
     * and make changes to ZipEntry.
677
     *
678
     * @param ZipEntry $zipEntry
679
     */
680 84
    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

680
    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...
681
    {
682 84
    }
683
684
    /**
685
     * @param ZipSourceFileData $zipFileData
686
     *
687
     * @throws ZipException
688
     * @throws Crc32Exception
689
     *
690
     * @return resource
691
     */
692 50
    public function getEntryStream(ZipSourceFileData $zipFileData)
693
    {
694 50
        $outStream = fopen('php://temp', 'w+b');
695 50
        $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

695
        $this->copyUncompressedDataToStream($zipFileData, /** @scrutinizer ignore-type */ $outStream);
Loading history...
696 49
        rewind($outStream);
697
698 49
        return $outStream;
699
    }
700
701
    /**
702
     * @param ZipSourceFileData $zipFileData
703
     * @param resource          $outStream
704
     *
705
     * @throws Crc32Exception
706
     * @throws ZipException
707
     */
708 53
    public function copyUncompressedDataToStream(ZipSourceFileData $zipFileData, $outStream)
709
    {
710 53
        if (!\is_resource($outStream)) {
711
            throw new InvalidArgumentException('outStream is not resource');
712
        }
713
714 53
        $entry = $zipFileData->getSourceEntry();
715
716
//        if ($entry->isDirectory()) {
717
//            throw new InvalidArgumentException('Streams not supported for directories');
718
//        }
719
720 53
        if ($entry->isStrongEncryption()) {
721
            throw new ZipException('Not support encryption zip.');
722
        }
723
724 53
        $compressionMethod = $entry->getCompressionMethod();
725
726 53
        fseek($this->inStream, $zipFileData->getOffset());
727
728 53
        $filters = [];
729
730 53
        $skipCheckCrc = false;
731 53
        $isEncrypted = $entry->isEncrypted();
732
733 53
        if ($isEncrypted) {
734 10
            if ($entry->getPassword() === null) {
735
                throw new ZipException('Can not password from entry ' . $entry->getName());
736
            }
737
738 10
            if (ZipEncryptionMethod::isWinZipAesMethod($entry->getEncryptionMethod())) {
739
                /** @var WinZipAesExtraField|null $winZipAesExtra */
740 8
                $winZipAesExtra = $entry->getExtraField(WinZipAesExtraField::HEADER_ID);
741
742 8
                if ($winZipAesExtra === null) {
743
                    throw new ZipException(
744
                        sprintf('WinZip AES must contain the extra field %s', WinZipAesExtraField::HEADER_ID)
745
                    );
746
                }
747 8
                $compressionMethod = $winZipAesExtra->getCompressionMethod();
748
749 8
                WinZipAesDecryptionStreamFilter::register();
750 8
                $cipherFilterName = WinZipAesDecryptionStreamFilter::FILTER_NAME;
751
752 8
                if ($winZipAesExtra->isV2()) {
753 8
                    $skipCheckCrc = true;
754
                }
755
            } else {
756 5
                PKDecryptionStreamFilter::register();
757 5
                $cipherFilterName = PKDecryptionStreamFilter::FILTER_NAME;
758
            }
759 10
            $encContextFilter = stream_filter_append(
760 10
                $this->inStream,
761
                $cipherFilterName,
762 10
                \STREAM_FILTER_READ,
763
                [
764 10
                    'entry' => $entry,
765
                ]
766
            );
767
768 10
            if (!$encContextFilter) {
0 ignored issues
show
introduced by
$encContextFilter is of type resource, thus it always evaluated to false.
Loading history...
769
                throw new \RuntimeException('Not apply filter ' . $cipherFilterName);
770
            }
771 10
            $filters[] = $encContextFilter;
772
        }
773
774
        // hack, see https://groups.google.com/forum/#!topic/alt.comp.lang.php/37_JZeW63uc
775 53
        $pos = ftell($this->inStream);
776 53
        rewind($this->inStream);
777 53
        fseek($this->inStream, $pos);
778
779 53
        $contextDecompress = null;
780 53
        switch ($compressionMethod) {
781
            case ZipCompressionMethod::STORED:
782
                // file without compression, do nothing
783 40
                break;
784
785
            case ZipCompressionMethod::DEFLATED:
786 17
                if (!($contextDecompress = stream_filter_append(
787 17
                    $this->inStream,
788 17
                    'zlib.inflate',
789 17
                    \STREAM_FILTER_READ
790
                ))) {
791
                    throw new \RuntimeException('Could not append filter "zlib.inflate" to stream');
792
                }
793 17
                $filters[] = $contextDecompress;
794
795 17
                break;
796
797
            case ZipCompressionMethod::BZIP2:
798 2
                if (!($contextDecompress = stream_filter_append(
799 2
                    $this->inStream,
800 2
                    'bzip2.decompress',
801 2
                    \STREAM_FILTER_READ
802
                ))) {
803
                    throw new \RuntimeException('Could not append filter "bzip2.decompress" to stream');
804
                }
805 2
                $filters[] = $contextDecompress;
806
807 2
                break;
808
809
            default:
810
                throw new ZipException(
811
                    sprintf(
812
                        '%s (compression method %d (%s) is not supported)',
813
                        $entry->getName(),
814
                        $compressionMethod,
815
                        ZipCompressionMethod::getCompressionMethodName($compressionMethod)
816
                    )
817
                );
818
        }
819
820 53
        $limit = $zipFileData->getUncompressedSize();
821
822 53
        $offset = 0;
823 53
        $chunkSize = 8192;
824
825
        try {
826 53
            if ($skipCheckCrc) {
0 ignored issues
show
introduced by
The condition $skipCheckCrc is always false.
Loading history...
827 6
                while ($offset < $limit) {
828 6
                    $length = min($chunkSize, $limit - $offset);
829 6
                    $buffer = fread($this->inStream, $length);
830
831 6
                    if ($buffer === false) {
832
                        throw new ZipException(sprintf('Error reading the contents of entry "%s".', $entry->getName()));
833
                    }
834 6
                    fwrite($outStream, $buffer);
835 6
                    $offset += $length;
836
                }
837
            } else {
838 52
                $contextHash = hash_init('crc32b');
839
840 52
                while ($offset < $limit) {
841 51
                    $length = min($chunkSize, $limit - $offset);
842 51
                    $buffer = fread($this->inStream, $length);
843
844 50
                    if ($buffer === false) {
845
                        throw new ZipException(sprintf('Error reading the contents of entry "%s".', $entry->getName()));
846
                    }
847 50
                    fwrite($outStream, $buffer);
848 50
                    hash_update($contextHash, $buffer);
849 50
                    $offset += $length;
850
                }
851
852 51
                $expectedCrc = (int) hexdec(hash_final($contextHash));
853
854 51
                if ($expectedCrc !== $entry->getCrc()) {
855 1
                    throw new Crc32Exception($entry->getName(), $expectedCrc, $entry->getCrc());
856
                }
857
            }
858 51
        } finally {
859 53
            for ($i = \count($filters); $i > 0; $i--) {
860 24
                stream_filter_remove($filters[$i - 1]);
861
            }
862
        }
863 51
    }
864
865
    /**
866
     * @param ZipSourceFileData $zipData
867
     * @param resource          $outStream
868
     */
869 17
    public function copyCompressedDataToStream(ZipSourceFileData $zipData, $outStream)
870
    {
871 17
        if ($zipData->getCompressedSize() > 0) {
872 17
            fseek($this->inStream, $zipData->getOffset());
873 17
            stream_copy_to_stream($this->inStream, $outStream, $zipData->getCompressedSize());
874
        }
875 17
    }
876
877
    /**
878
     * @return bool
879
     */
880 1
    protected function isZip64Support()
881
    {
882 1
        return \PHP_INT_SIZE === 8; // true for 64bit system
883
    }
884
885 91
    public function close()
886
    {
887 91
        if (\is_resource($this->inStream)) {
888 88
            fclose($this->inStream);
889
        }
890 91
    }
891
892 8
    public function __destruct()
893
    {
894 8
        $this->close();
895 8
    }
896
}
897