Passed
Push — master ( 3b7697...25d5dd )
by Alexey
03:48 queued 29s
created

ZipReader::close()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
cc 2
eloc 2
c 1
b 0
f 1
nc 2
nop 0
dl 0
loc 4
rs 10
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
    public function __construct($inStream, array $options = [])
51
    {
52
        if (!\is_resource($inStream)) {
53
            throw new InvalidArgumentException('Stream must be a resource');
54
        }
55
        $type = get_resource_type($inStream);
56
57
        if ($type !== 'stream') {
58
            throw new InvalidArgumentException("Invalid resource type {$type}.");
59
        }
60
        $meta = stream_get_meta_data($inStream);
61
62
        $wrapperType = isset($meta['wrapper_type']) ? $meta['wrapper_type'] : 'Unknown';
63
        $supportStreamWrapperTypes = ['plainfile', 'PHP', 'user-space'];
64
65
        if (!\in_array($wrapperType, $supportStreamWrapperTypes, true)) {
66
            throw new InvalidArgumentException(
67
                'The stream wrapper type "' . $wrapperType . '" is not supported. Support: ' . implode(
68
                    ', ',
69
                    $supportStreamWrapperTypes
70
                )
71
            );
72
        }
73
74
        if (
75
            $wrapperType === 'plainfile' &&
76
            (
77
                $meta['stream_type'] === 'dir' ||
78
                (isset($meta['uri']) && is_dir($meta['uri']))
79
            )
80
        ) {
81
            throw new InvalidArgumentException('Directory stream not supported');
82
        }
83
84
        $seekable = $meta['seekable'];
85
86
        if (!$seekable) {
87
            throw new InvalidArgumentException('Resource does not support seekable.');
88
        }
89
        $this->size = fstat($inStream)['size'];
90
        $this->inStream = $inStream;
91
92
        /** @noinspection AdditionOperationOnArraysInspection */
93
        $options += $this->getDefaultOptions();
94
        $this->options = $options;
95
    }
96
97
    /**
98
     * @return array
99
     */
100
    protected function getDefaultOptions()
101
    {
102
        return [
103
            ZipOptions::CHARSET => null,
104
        ];
105
    }
106
107
    /**
108
     * @throws ZipException
109
     *
110
     * @return ImmutableZipContainer
111
     */
112
    public function read()
113
    {
114
        if ($this->size < ZipConstants::END_CD_MIN_LEN) {
115
            throw new ZipException('Corrupt zip file');
116
        }
117
118
        $endOfCentralDirectory = $this->readEndOfCentralDirectory();
119
        $entries = $this->readCentralDirectory($endOfCentralDirectory);
120
121
        return new ImmutableZipContainer($entries, $endOfCentralDirectory->getComment());
122
    }
123
124
    /**
125
     * @return array
126
     */
127
    public function getStreamMetaData()
128
    {
129
        return stream_get_meta_data($this->inStream);
130
    }
131
132
    /**
133
     * Read End of central directory record.
134
     *
135
     * end of central dir signature    4 bytes  (0x06054b50)
136
     * number of this disk             2 bytes
137
     * number of the disk with the
138
     * start of the central directory  2 bytes
139
     * total number of entries in the
140
     * central directory on this disk  2 bytes
141
     * total number of entries in
142
     * the central directory           2 bytes
143
     * size of the central directory   4 bytes
144
     * offset of start of central
145
     * directory with respect to
146
     * the starting disk number        4 bytes
147
     * .ZIP file comment length        2 bytes
148
     * .ZIP file comment       (variable size)
149
     *
150
     * @throws ZipException
151
     *
152
     * @return EndOfCentralDirectory
153
     */
154
    protected function readEndOfCentralDirectory()
155
    {
156
        if (!$this->findEndOfCentralDirectory()) {
157
            throw new ZipException('Invalid zip file. The end of the central directory could not be found.');
158
        }
159
160
        $positionECD = ftell($this->inStream) - 4;
161
        $sizeECD = $this->size - ftell($this->inStream);
162
        $buffer = fread($this->inStream, $sizeECD);
163
164
        $unpack = unpack(
165
            'vdiskNo/vcdDiskNo/vcdEntriesDisk/' .
166
            'vcdEntries/VcdSize/VcdPos/vcommentLength',
167
            substr($buffer, 0, 18)
168
        );
169
170
        if (
171
            $unpack['diskNo'] !== 0 ||
172
            $unpack['cdDiskNo'] !== 0 ||
173
            $unpack['cdEntriesDisk'] !== $unpack['cdEntries']
174
        ) {
175
            throw new ZipException(
176
                'ZIP file spanning/splitting is not supported!'
177
            );
178
        }
179
        // .ZIP file comment       (variable sizeECD)
180
        $comment = null;
181
182
        if ($unpack['commentLength'] > 0) {
183
            $comment = substr($buffer, 18, $unpack['commentLength']);
184
        }
185
186
        // Check for ZIP64 End Of Central Directory Locator exists.
187
        $zip64ECDLocatorPosition = $positionECD - ZipConstants::ZIP64_END_CD_LOC_LEN;
188
        fseek($this->inStream, $zip64ECDLocatorPosition);
189
        // zip64 end of central dir locator
190
        // signature                       4 bytes  (0x07064b50)
191
        if ($zip64ECDLocatorPosition > 0 && unpack(
192
            'V',
193
            fread($this->inStream, 4)
194
        )[1] === ZipConstants::ZIP64_END_CD_LOC) {
195
            if (!$this->isZip64Support()) {
196
                throw new ZipException('ZIP64 not supported this archive.');
197
            }
198
199
            $positionECD = $this->findZip64ECDPosition();
200
            $endCentralDirectory = $this->readZip64EndOfCentralDirectory($positionECD);
201
            $endCentralDirectory->setComment($comment);
202
        } else {
203
            $endCentralDirectory = new EndOfCentralDirectory(
204
                $unpack['cdEntries'],
205
                $unpack['cdPos'],
206
                $unpack['cdSize'],
207
                false,
208
                $comment
209
            );
210
        }
211
212
        return $endCentralDirectory;
213
    }
214
215
    /**
216
     * @return bool
217
     */
218
    protected function findEndOfCentralDirectory()
219
    {
220
        $max = $this->size - ZipConstants::END_CD_MIN_LEN;
221
        $min = $max >= 0xffff ? $max - 0xffff : 0;
222
        // Search for End of central directory record.
223
        for ($position = $max; $position >= $min; $position--) {
224
            fseek($this->inStream, $position);
225
            // end of central dir signature    4 bytes  (0x06054b50)
226
            if (unpack('V', fread($this->inStream, 4))[1] !== ZipConstants::END_CD) {
227
                continue;
228
            }
229
230
            return true;
231
        }
232
233
        return false;
234
    }
235
236
    /**
237
     * Read Zip64 end of central directory locator and returns
238
     * Zip64 end of central directory position.
239
     *
240
     * number of the disk with the
241
     * start of the zip64 end of
242
     * central directory               4 bytes
243
     * relative offset of the zip64
244
     * end of central directory record 8 bytes
245
     * total number of disks           4 bytes
246
     *
247
     * @throws ZipException
248
     *
249
     * @return int Zip64 End Of Central Directory position
250
     */
251
    protected function findZip64ECDPosition()
252
    {
253
        $diskNo = unpack('V', fread($this->inStream, 4))[1];
254
        $zip64ECDPos = PackUtil::unpackLongLE(fread($this->inStream, 8));
255
        $totalDisks = unpack('V', fread($this->inStream, 4))[1];
256
257
        if ($diskNo !== 0 || $totalDisks > 1) {
258
            throw new ZipException('ZIP file spanning/splitting is not supported!');
259
        }
260
261
        return $zip64ECDPos;
262
    }
263
264
    /**
265
     * Read zip64 end of central directory locator and zip64 end
266
     * of central directory record.
267
     *
268
     * zip64 end of central dir
269
     * signature                       4 bytes  (0x06064b50)
270
     * size of zip64 end of central
271
     * directory record                8 bytes
272
     * version made by                 2 bytes
273
     * version needed to extract       2 bytes
274
     * number of this disk             4 bytes
275
     * number of the disk with the
276
     * start of the central directory  4 bytes
277
     * total number of entries in the
278
     * central directory on this disk  8 bytes
279
     * total number of entries in the
280
     * central directory               8 bytes
281
     * size of the central directory   8 bytes
282
     * offset of start of central
283
     * directory with respect to
284
     * the starting disk number        8 bytes
285
     * zip64 extensible data sector    (variable size)
286
     *
287
     * @param int $zip64ECDPosition
288
     *
289
     * @throws ZipException
290
     *
291
     * @return EndOfCentralDirectory
292
     */
293
    protected function readZip64EndOfCentralDirectory($zip64ECDPosition)
294
    {
295
        fseek($this->inStream, $zip64ECDPosition);
296
297
        $buffer = fread($this->inStream, ZipConstants::ZIP64_END_OF_CD_LEN);
298
299
        if (unpack('V', $buffer)[1] !== ZipConstants::ZIP64_END_CD) {
300
            throw new ZipException('Expected ZIP64 End Of Central Directory Record!');
301
        }
302
303
        $data = unpack(
304
//            'Psize/vversionMadeBy/vextractVersion/' .
305
            'VdiskNo/VcdDiskNo',
306
            substr($buffer, 16, 8)
307
        );
308
309
        $cdEntriesDisk = PackUtil::unpackLongLE(substr($buffer, 24, 8));
310
        $entryCount = PackUtil::unpackLongLE(substr($buffer, 32, 8));
311
        $cdSize = PackUtil::unpackLongLE(substr($buffer, 40, 8));
312
        $cdPos = PackUtil::unpackLongLE(substr($buffer, 48, 8));
313
314
//        $platform = ZipPlatform::fromValue(($data['versionMadeBy'] & 0xFF00) >> 8);
315
//        $softwareVersion = $data['versionMadeBy'] & 0x00FF;
316
317
        if ($data['diskNo'] !== 0 || $data['cdDiskNo'] !== 0 || $entryCount !== $cdEntriesDisk) {
318
            throw new ZipException('ZIP file spanning/splitting is not supported!');
319
        }
320
321
        if ($entryCount < 0 || $entryCount > 0x7fffffff) {
322
            throw new ZipException('Total Number Of Entries In The Central Directory out of range!');
323
        }
324
325
        // skip zip64 extensible data sector (variable sizeEndCD)
326
327
        return new EndOfCentralDirectory(
328
            $entryCount,
329
            $cdPos,
330
            $cdSize,
331
            true
332
        );
333
    }
334
335
    /**
336
     * Reads the central directory from the given seekable byte channel
337
     * and populates the internal tables with ZipEntry instances.
338
     *
339
     * The ZipEntry's will know all data that can be obtained from the
340
     * central directory alone, but not the data that requires the local
341
     * file header or additional data to be read.
342
     *
343
     * @param EndOfCentralDirectory $endCD
344
     *
345
     * @throws ZipException
346
     *
347
     * @return ZipEntry[]
348
     */
349
    protected function readCentralDirectory(EndOfCentralDirectory $endCD)
350
    {
351
        $entries = [];
352
353
        $cdOffset = $endCD->getCdOffset();
354
        fseek($this->inStream, $cdOffset);
355
356
        if (!($cdStream = fopen('php://temp', 'w+b'))) {
357
            throw new ZipException('Temp resource can not open from write');
358
        }
359
        stream_copy_to_stream($this->inStream, $cdStream, $endCD->getCdSize());
360
        rewind($cdStream);
361
        for ($numEntries = $endCD->getEntryCount(); $numEntries > 0; $numEntries--) {
362
            $zipEntry = $this->readZipEntry($cdStream);
363
364
            $entryName = $zipEntry->getName();
365
366
            /** @var UnicodePathExtraField|null $unicodePathExtraField */
367
            $unicodePathExtraField = $zipEntry->getExtraField(UnicodePathExtraField::HEADER_ID);
368
369
            if ($unicodePathExtraField !== null) {
370
                $unicodePath = $unicodePathExtraField->getUnicodeValue();
371
372
                if ($unicodePath !== null) {
373
                    $unicodePath = str_replace('\\', '/', $unicodePath);
374
375
                    if (
376
                        $unicodePath !== '' &&
377
                        substr_count($entryName, '/') === substr_count($unicodePath, '/')
378
                    ) {
379
                        $entryName = $unicodePath;
380
                    }
381
                }
382
            }
383
384
            $entries[$entryName] = $zipEntry;
385
        }
386
387
        return $entries;
388
    }
389
390
    /**
391
     * Read central directory entry.
392
     *
393
     * central file header signature   4 bytes  (0x02014b50)
394
     * version made by                 2 bytes
395
     * version needed to extract       2 bytes
396
     * general purpose bit flag        2 bytes
397
     * compression method              2 bytes
398
     * last mod file time              2 bytes
399
     * last mod file date              2 bytes
400
     * crc-32                          4 bytes
401
     * compressed size                 4 bytes
402
     * uncompressed size               4 bytes
403
     * file name length                2 bytes
404
     * extra field length              2 bytes
405
     * file comment length             2 bytes
406
     * disk number start               2 bytes
407
     * internal file attributes        2 bytes
408
     * external file attributes        4 bytes
409
     * relative offset of local header 4 bytes
410
     *
411
     * file name (variable size)
412
     * extra field (variable size)
413
     * file comment (variable size)
414
     *
415
     * @param resource $stream
416
     *
417
     * @throws ZipException
418
     *
419
     * @return ZipEntry
420
     */
421
    protected function readZipEntry($stream)
422
    {
423
        if (unpack('V', fread($stream, 4))[1] !== ZipConstants::CENTRAL_FILE_HEADER) {
424
            throw new ZipException('Corrupt zip file. Cannot read zip entry.');
425
        }
426
427
        $unpack = unpack(
428
            'vversionMadeBy/vversionNeededToExtract/' .
429
            'vgeneralPurposeBitFlag/vcompressionMethod/' .
430
            'VlastModFile/Vcrc/VcompressedSize/' .
431
            'VuncompressedSize/vfileNameLength/vextraFieldLength/' .
432
            'vfileCommentLength/vdiskNumberStart/vinternalFileAttributes/' .
433
            'VexternalFileAttributes/VoffsetLocalHeader',
434
            fread($stream, 42)
435
        );
436
437
        if ($unpack['diskNumberStart'] !== 0) {
438
            throw new ZipException('ZIP file spanning/splitting is not supported!');
439
        }
440
441
        $generalPurposeBitFlags = $unpack['generalPurposeBitFlag'];
442
        $isUtf8 = ($generalPurposeBitFlags & GeneralPurposeBitFlag::UTF8) !== 0;
443
444
        $name = fread($stream, $unpack['fileNameLength']);
445
446
        $createdOS = ($unpack['versionMadeBy'] & 0xFF00) >> 8;
447
        $softwareVersion = $unpack['versionMadeBy'] & 0x00FF;
448
449
        $extractedOS = ($unpack['versionNeededToExtract'] & 0xFF00) >> 8;
450
        $extractVersion = $unpack['versionNeededToExtract'] & 0x00FF;
451
452
        $dosTime = $unpack['lastModFile'];
453
454
        $comment = null;
455
456
        if ($unpack['fileCommentLength'] > 0) {
457
            $comment = fread($stream, $unpack['fileCommentLength']);
458
        }
459
460
        // decode code page names
461
        $fallbackCharset = null;
462
463
        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
        $zipEntry = ZipEntry::create(
475
            $name,
476
            $createdOS,
477
            $extractedOS,
478
            $softwareVersion,
479
            $extractVersion,
480
            $unpack['compressionMethod'],
481
            $generalPurposeBitFlags,
482
            $dosTime,
483
            $unpack['crc'],
484
            $unpack['compressedSize'],
485
            $unpack['uncompressedSize'],
486
            $unpack['internalFileAttributes'],
487
            $unpack['externalFileAttributes'],
488
            $unpack['offsetLocalHeader'],
489
            $comment,
490
            $fallbackCharset
491
        );
492
493
        if ($unpack['extraFieldLength'] > 0) {
494
            $this->parseExtraFields(
495
                fread($stream, $unpack['extraFieldLength']),
496
                $zipEntry,
497
                false
498
            );
499
500
            /** @var Zip64ExtraField|null $extraZip64 */
501
            $extraZip64 = $zipEntry->getCdExtraField(Zip64ExtraField::HEADER_ID);
502
503
            if ($extraZip64 !== null) {
504
                $this->handleZip64Extra($extraZip64, $zipEntry);
505
            }
506
        }
507
508
        $this->loadLocalExtraFields($zipEntry);
509
        $this->handleExtraEncryptionFields($zipEntry);
510
        $this->handleExtraFields($zipEntry);
511
512
        return $zipEntry;
513
    }
514
515
    /**
516
     * @param string   $buffer
517
     * @param ZipEntry $zipEntry
518
     * @param bool     $local
519
     *
520
     * @return ExtraFieldsCollection
521
     */
522
    protected function parseExtraFields($buffer, ZipEntry $zipEntry, $local = false)
523
    {
524
        $collection = $local ?
525
            $zipEntry->getLocalExtraFields() :
526
            $zipEntry->getCdExtraFields();
527
528
        if (!empty($buffer)) {
529
            $pos = 0;
530
            $endPos = \strlen($buffer);
531
532
            while ($endPos - $pos >= 4) {
533
                /** @var int[] $data */
534
                $data = unpack('vheaderId/vdataSize', substr($buffer, $pos, 4));
535
                $pos += 4;
536
537
                if ($endPos - $pos - $data['dataSize'] < 0) {
538
                    break;
539
                }
540
                $bufferData = substr($buffer, $pos, $data['dataSize']);
541
                $headerId = $data['headerId'];
542
543
                /** @var string|ZipExtraField|null $className */
544
                $className = ZipExtraDriver::getClassNameOrNull($headerId);
545
546
                if ($className !== null) {
547
                    try {
548
                        $extraField = $local ?
549
                            \call_user_func([$className, 'unpackLocalFileData'], $bufferData, $zipEntry) :
550
                            \call_user_func([$className, 'unpackCentralDirData'], $bufferData, $zipEntry);
551
                    } catch (\Throwable $e) {
552
                        throw new \RuntimeException(
553
                            sprintf(
554
                                'Error parse %s extra field 0x%04X',
555
                                $local ? 'local' : 'central directory',
556
                                $headerId
557
                            )
558
                        );
559
                    }
560
                } else {
561
                    $extraField = new UnrecognizedExtraField($headerId, $bufferData);
562
                }
563
                $collection->add($extraField);
564
                $pos += $data['dataSize'];
565
            }
566
        }
567
568
        return $collection;
569
    }
570
571
    /**
572
     * @param Zip64ExtraField $extraZip64
573
     * @param ZipEntry        $zipEntry
574
     */
575
    protected function handleZip64Extra(Zip64ExtraField $extraZip64, ZipEntry $zipEntry)
576
    {
577
        $uncompressedSize = $extraZip64->getUncompressedSize();
578
        $compressedSize = $extraZip64->getCompressedSize();
579
        $localHeaderOffset = $extraZip64->getLocalHeaderOffset();
580
581
        if ($uncompressedSize !== null) {
582
            $zipEntry->setUncompressedSize($uncompressedSize);
583
        }
584
585
        if ($compressedSize !== null) {
586
            $zipEntry->setCompressedSize($compressedSize);
587
        }
588
589
        if ($localHeaderOffset !== null) {
590
            $zipEntry->setLocalHeaderOffset($localHeaderOffset);
591
        }
592
    }
593
594
    /**
595
     * Read Local File Header.
596
     *
597
     * local file header signature     4 bytes  (0x04034b50)
598
     * version needed to extract       2 bytes
599
     * general purpose bit flag        2 bytes
600
     * compression method              2 bytes
601
     * last mod file time              2 bytes
602
     * last mod file date              2 bytes
603
     * crc-32                          4 bytes
604
     * compressed size                 4 bytes
605
     * uncompressed size               4 bytes
606
     * file name length                2 bytes
607
     * extra field length              2 bytes
608
     * file name (variable size)
609
     * extra field (variable size)
610
     *
611
     * @param ZipEntry $entry
612
     *
613
     * @throws ZipException
614
     */
615
    protected function loadLocalExtraFields(ZipEntry $entry)
616
    {
617
        $offsetLocalHeader = $entry->getLocalHeaderOffset();
618
619
        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
        }
624
625
        fseek($this->inStream, $offsetLocalHeader + ZipConstants::LFH_FILENAME_LENGTH_POS);
626
        $unpack = unpack('vfileNameLength/vextraFieldLength', fread($this->inStream, 4));
627
        $offsetData = ftell($this->inStream)
628
            + $unpack['fileNameLength']
629
            + $unpack['extraFieldLength'];
630
631
        fseek($this->inStream, $unpack['fileNameLength'], \SEEK_CUR);
632
633
        if ($unpack['extraFieldLength'] > 0) {
634
            $this->parseExtraFields(
635
                fread($this->inStream, $unpack['extraFieldLength']),
636
                $entry,
637
                true
638
            );
639
        }
640
641
        $zipData = new ZipSourceFileData($this, $entry, $offsetData);
642
        $entry->setData($zipData);
643
    }
644
645
    /**
646
     * @param ZipEntry $zipEntry
647
     *
648
     * @throws ZipException
649
     */
650
    private function handleExtraEncryptionFields(ZipEntry $zipEntry)
651
    {
652
        if ($zipEntry->isEncrypted()) {
653
            if ($zipEntry->getCompressionMethod() === ZipCompressionMethod::WINZIP_AES) {
654
                /** @var WinZipAesExtraField|null $extraField */
655
                $extraField = $zipEntry->getExtraField(WinZipAesExtraField::HEADER_ID);
656
657
                if ($extraField === null) {
658
                    throw new ZipException(
659
                        sprintf(
660
                            'Extra field 0x%04x (WinZip-AES Encryption) expected for compression method %d',
661
                            WinZipAesExtraField::HEADER_ID,
662
                            $zipEntry->getCompressionMethod()
663
                        )
664
                    );
665
                }
666
                $zipEntry->setCompressionMethod($extraField->getCompressionMethod());
667
                $zipEntry->setEncryptionMethod($extraField->getEncryptionMethod());
668
            } else {
669
                $zipEntry->setEncryptionMethod(ZipEncryptionMethod::PKWARE);
670
            }
671
        }
672
    }
673
674
    /**
675
     * Handle extra data in zip records.
676
     *
677
     * This is a special method in which you can process ExtraField
678
     * and make changes to ZipEntry.
679
     *
680
     * @param ZipEntry $zipEntry
681
     */
682
    protected function handleExtraFields(ZipEntry $zipEntry)
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
     * @return resource
693
     */
694
    public function getEntryStream(ZipSourceFileData $zipFileData)
695
    {
696
        $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
        rewind($outStream);
699
700
        return $outStream;
701
    }
702
703
    /**
704
     * @param ZipSourceFileData $zipFileData
705
     * @param resource          $outStream
706
     *
707
     * @throws Crc32Exception
708
     * @throws ZipException
709
     */
710
    public function copyUncompressedDataToStream(ZipSourceFileData $zipFileData, $outStream)
711
    {
712
        if (!\is_resource($outStream)) {
713
            throw new InvalidArgumentException('outStream is not resource');
714
        }
715
716
        $entry = $zipFileData->getSourceEntry();
717
718
//        if ($entry->isDirectory()) {
719
//            throw new InvalidArgumentException('Streams not supported for directories');
720
//        }
721
722
        if ($entry->isStrongEncryption()) {
723
            throw new ZipException('Not support encryption zip.');
724
        }
725
726
        $compressionMethod = $entry->getCompressionMethod();
727
728
        fseek($this->inStream, $zipFileData->getOffset());
729
730
        $filters = [];
731
732
        $skipCheckCrc = false;
733
        $isEncrypted = $entry->isEncrypted();
734
735
        if ($isEncrypted) {
736
            if ($entry->getPassword() === null) {
737
                throw new ZipException('Can not password from entry ' . $entry->getName());
738
            }
739
740
            if (ZipEncryptionMethod::isWinZipAesMethod($entry->getEncryptionMethod())) {
741
                /** @var WinZipAesExtraField|null $winZipAesExtra */
742
                $winZipAesExtra = $entry->getExtraField(WinZipAesExtraField::HEADER_ID);
743
744
                if ($winZipAesExtra === null) {
745
                    throw new ZipException(
746
                        sprintf('WinZip AES must contain the extra field %s', WinZipAesExtraField::HEADER_ID)
747
                    );
748
                }
749
                $compressionMethod = $winZipAesExtra->getCompressionMethod();
750
751
                WinZipAesDecryptionStreamFilter::register();
752
                $cipherFilterName = WinZipAesDecryptionStreamFilter::FILTER_NAME;
753
754
                if ($winZipAesExtra->isV2()) {
755
                    $skipCheckCrc = true;
756
                }
757
            } else {
758
                PKDecryptionStreamFilter::register();
759
                $cipherFilterName = PKDecryptionStreamFilter::FILTER_NAME;
760
            }
761
            $encContextFilter = stream_filter_append(
762
                $this->inStream,
763
                $cipherFilterName,
764
                \STREAM_FILTER_READ,
765
                [
766
                    'entry' => $entry,
767
                ]
768
            );
769
770
            if (!$encContextFilter) {
0 ignored issues
show
introduced by
$encContextFilter is of type resource, thus it always evaluated to false.
Loading history...
771
                throw new \RuntimeException('Not apply filter ' . $cipherFilterName);
772
            }
773
            $filters[] = $encContextFilter;
774
        }
775
776
        // hack, see https://groups.google.com/forum/#!topic/alt.comp.lang.php/37_JZeW63uc
777
        $pos = ftell($this->inStream);
778
        rewind($this->inStream);
779
        fseek($this->inStream, $pos);
780
781
        $contextDecompress = null;
782
        switch ($compressionMethod) {
783
            case ZipCompressionMethod::STORED:
784
                // file without compression, do nothing
785
                break;
786
787
            case ZipCompressionMethod::DEFLATED:
788
                if (!($contextDecompress = stream_filter_append(
789
                    $this->inStream,
790
                    'zlib.inflate',
791
                    \STREAM_FILTER_READ
792
                ))) {
793
                    throw new \RuntimeException('Could not append filter "zlib.inflate" to stream');
794
                }
795
                $filters[] = $contextDecompress;
796
797
                break;
798
799
            case ZipCompressionMethod::BZIP2:
800
                if (!($contextDecompress = stream_filter_append(
801
                    $this->inStream,
802
                    'bzip2.decompress',
803
                    \STREAM_FILTER_READ
804
                ))) {
805
                    throw new \RuntimeException('Could not append filter "bzip2.decompress" to stream');
806
                }
807
                $filters[] = $contextDecompress;
808
809
                break;
810
811
            default:
812
                throw new ZipException(
813
                    sprintf(
814
                        '%s (compression method %d (%s) is not supported)',
815
                        $entry->getName(),
816
                        $compressionMethod,
817
                        ZipCompressionMethod::getCompressionMethodName($compressionMethod)
818
                    )
819
                );
820
        }
821
822
        $limit = $zipFileData->getUncompressedSize();
823
824
        $offset = 0;
825
        $chunkSize = 8192;
826
827
        try {
828
            if ($skipCheckCrc) {
0 ignored issues
show
introduced by
The condition $skipCheckCrc is always false.
Loading history...
829
                while ($offset < $limit) {
830
                    $length = min($chunkSize, $limit - $offset);
831
                    $buffer = fread($this->inStream, $length);
832
833
                    if ($buffer === false) {
834
                        throw new ZipException(sprintf('Error reading the contents of entry "%s".', $entry->getName()));
835
                    }
836
                    fwrite($outStream, $buffer);
837
                    $offset += $length;
838
                }
839
            } else {
840
                $contextHash = hash_init('crc32b');
841
842
                while ($offset < $limit) {
843
                    $length = min($chunkSize, $limit - $offset);
844
                    $buffer = fread($this->inStream, $length);
845
846
                    if ($buffer === false) {
847
                        throw new ZipException(sprintf('Error reading the contents of entry "%s".', $entry->getName()));
848
                    }
849
                    fwrite($outStream, $buffer);
850
                    hash_update($contextHash, $buffer);
851
                    $offset += $length;
852
                }
853
854
                $expectedCrc = (int) hexdec(hash_final($contextHash));
855
856
                if ($expectedCrc !== $entry->getCrc()) {
857
                    throw new Crc32Exception($entry->getName(), $expectedCrc, $entry->getCrc());
858
                }
859
            }
860
        } finally {
861
            for ($i = \count($filters); $i > 0; $i--) {
862
                stream_filter_remove($filters[$i - 1]);
863
            }
864
        }
865
    }
866
867
    /**
868
     * @param ZipSourceFileData $zipData
869
     * @param resource          $outStream
870
     */
871
    public function copyCompressedDataToStream(ZipSourceFileData $zipData, $outStream)
872
    {
873
        if ($zipData->getCompressedSize() > 0) {
874
            fseek($this->inStream, $zipData->getOffset());
875
            stream_copy_to_stream($this->inStream, $outStream, $zipData->getCompressedSize());
876
        }
877
    }
878
879
    /**
880
     * @return bool
881
     */
882
    protected function isZip64Support()
883
    {
884
        return \PHP_INT_SIZE === 8; // true for 64bit system
885
    }
886
887
    public function close()
888
    {
889
        if (\is_resource($this->inStream)) {
890
            fclose($this->inStream);
891
        }
892
    }
893
894
    public function __destruct()
895
    {
896
        $this->close();
897
    }
898
}
899