Passed
Push — master ( ca068f...f2d295 )
by Alexey
03:12 queued 16s
created

ZipInputStream::readCentralDirectoryEntry()   A

Complexity

Conditions 4
Paths 5

Size

Total Lines 55
Code Lines 39

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 39
c 0
b 0
f 0
nc 5
nop 1
dl 0
loc 55
rs 9.296

How to fix   Long Method   

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\Stream;
4
5
use PhpZip\Crypto\TraditionalPkwareEncryptionEngine;
6
use PhpZip\Crypto\WinZipAesEngine;
7
use PhpZip\Exception\Crc32Exception;
8
use PhpZip\Exception\InvalidArgumentException;
9
use PhpZip\Exception\RuntimeException;
10
use PhpZip\Exception\ZipAuthenticationException;
11
use PhpZip\Exception\ZipException;
12
use PhpZip\Exception\ZipUnsupportMethodException;
13
use PhpZip\Extra\ExtraFieldsCollection;
14
use PhpZip\Extra\ExtraFieldsFactory;
15
use PhpZip\Extra\Fields\ApkAlignmentExtraField;
16
use PhpZip\Extra\Fields\WinZipAesEntryExtraField;
17
use PhpZip\Model\EndOfCentralDirectory;
18
use PhpZip\Model\Entry\ZipSourceEntry;
19
use PhpZip\Model\ZipEntry;
20
use PhpZip\Model\ZipModel;
21
use PhpZip\Util\PackUtil;
22
use PhpZip\Util\StringUtil;
23
use PhpZip\ZipFile;
24
25
/**
26
 * Read zip file.
27
 *
28
 * @author Ne-Lexa [email protected]
29
 * @license MIT
30
 */
31
class ZipInputStream implements ZipInputStreamInterface
32
{
33
    /** @var resource */
34
    protected $in;
35
36
    /** @var ZipModel */
37
    protected $zipModel;
38
39
    /**
40
     * ZipInputStream constructor.
41
     *
42
     * @param resource $in
43
     */
44
    public function __construct($in)
45
    {
46
        if (!\is_resource($in)) {
47
            throw new RuntimeException('$in must be resource');
48
        }
49
        $this->in = $in;
50
    }
51
52
    /**
53
     * @throws ZipException
54
     *
55
     * @return ZipModel
56
     */
57
    public function readZip()
58
    {
59
        $this->checkZipFileSignature();
60
        $endOfCentralDirectory = $this->readEndOfCentralDirectory();
61
        $entries = $this->mountCentralDirectory($endOfCentralDirectory);
62
        $this->zipModel = ZipModel::newSourceModel($entries, $endOfCentralDirectory);
63
64
        return $this->zipModel;
65
    }
66
67
    /**
68
     * Check zip file signature.
69
     *
70
     * @throws ZipException if this not .ZIP file.
71
     */
72
    protected function checkZipFileSignature()
73
    {
74
        rewind($this->in);
75
        // Constraint: A ZIP file must start with a Local File Header
76
        // or a (ZIP64) End Of Central Directory Record if it's empty.
77
        $signatureBytes = fread($this->in, 4);
78
79
        if (\strlen($signatureBytes) < 4) {
80
            throw new ZipException('Invalid zip file.');
81
        }
82
        $signature = unpack('V', $signatureBytes)[1];
83
84
        if (
85
            $signature !== ZipEntry::LOCAL_FILE_HEADER_SIG
86
            && $signature !== EndOfCentralDirectory::ZIP64_END_OF_CD_RECORD_SIG
87
            && $signature !== EndOfCentralDirectory::END_OF_CD_SIG
88
        ) {
89
            throw new ZipException(
90
                'Expected Local File Header or (ZIP64) End Of Central Directory Record! Signature: ' . $signature
91
            );
92
        }
93
    }
94
95
    /**
96
     * @throws ZipException
97
     *
98
     * @return EndOfCentralDirectory
99
     */
100
    protected function readEndOfCentralDirectory()
101
    {
102
        if (!$this->findEndOfCentralDirectory()) {
103
            throw new ZipException('Invalid zip file. The end of the central directory could not be found.');
104
        }
105
106
        $positionECD = ftell($this->in) - 4;
107
        $buffer = fread($this->in, fstat($this->in)['size'] - $positionECD);
108
109
        $unpack = unpack(
110
            'vdiskNo/vcdDiskNo/vcdEntriesDisk/' .
111
            'vcdEntries/VcdSize/VcdPos/vcommentLength',
112
            substr($buffer, 0, 18)
113
        );
114
115
        if (
116
            $unpack['diskNo'] !== 0 ||
117
            $unpack['cdDiskNo'] !== 0 ||
118
            $unpack['cdEntriesDisk'] !== $unpack['cdEntries']
119
        ) {
120
            throw new ZipException(
121
                'ZIP file spanning/splitting is not supported!'
122
            );
123
        }
124
        // .ZIP file comment       (variable sizeECD)
125
        $comment = null;
126
127
        if ($unpack['commentLength'] > 0) {
128
            $comment = substr($buffer, 18, $unpack['commentLength']);
129
        }
130
131
        // Check for ZIP64 End Of Central Directory Locator exists.
132
        $zip64ECDLocatorPosition = $positionECD - EndOfCentralDirectory::ZIP64_END_OF_CD_LOCATOR_LEN;
133
        fseek($this->in, $zip64ECDLocatorPosition);
134
        // zip64 end of central dir locator
135
        // signature                       4 bytes  (0x07064b50)
136
        if ($zip64ECDLocatorPosition > 0 && unpack(
137
            'V',
138
            fread($this->in, 4)
139
        )[1] === EndOfCentralDirectory::ZIP64_END_OF_CD_LOCATOR_SIG) {
140
            $positionECD = $this->findZip64ECDPosition();
141
            $endCentralDirectory = $this->readZip64EndOfCentralDirectory($positionECD);
142
            $endCentralDirectory->setComment($comment);
143
        } else {
144
            $endCentralDirectory = new EndOfCentralDirectory(
145
                $unpack['cdEntries'],
146
                $unpack['cdPos'],
147
                $unpack['cdSize'],
148
                false,
149
                $comment
150
            );
151
        }
152
153
        return $endCentralDirectory;
154
    }
155
156
    /**
157
     * @throws ZipException
158
     *
159
     * @return bool
160
     */
161
    protected function findEndOfCentralDirectory()
162
    {
163
        $max = fstat($this->in)['size'] - EndOfCentralDirectory::END_OF_CENTRAL_DIRECTORY_RECORD_MIN_LEN;
164
165
        if ($max < 0) {
166
            throw new ZipException('Too short to be a zip file');
167
        }
168
        $min = $max >= 0xffff ? $max - 0xffff : 0;
169
        // Search for End of central directory record.
170
        for ($position = $max; $position >= $min; $position--) {
171
            fseek($this->in, $position);
172
            // end of central dir signature    4 bytes  (0x06054b50)
173
            if (unpack('V', fread($this->in, 4))[1] !== EndOfCentralDirectory::END_OF_CD_SIG) {
174
                continue;
175
            }
176
177
            return true;
178
        }
179
180
        return false;
181
    }
182
183
    /**
184
     * Read Zip64 end of central directory locator and returns
185
     * Zip64 end of central directory position.
186
     *
187
     * number of the disk with the
188
     * start of the zip64 end of
189
     * central directory               4 bytes
190
     * relative offset of the zip64
191
     * end of central directory record 8 bytes
192
     * total number of disks           4 bytes
193
     *
194
     * @throws ZipException
195
     *
196
     * @return int Zip64 End Of Central Directory position
197
     */
198
    protected function findZip64ECDPosition()
199
    {
200
        $diskNo = unpack('V', fread($this->in, 4))[1];
201
        $zip64ECDPos = PackUtil::unpackLongLE(fread($this->in, 8));
202
        $totalDisks = unpack('V', fread($this->in, 4))[1];
203
204
        if ($diskNo !== 0 || $totalDisks > 1) {
205
            throw new ZipException('ZIP file spanning/splitting is not supported!');
206
        }
207
208
        return $zip64ECDPos;
209
    }
210
211
    /**
212
     * Read zip64 end of central directory locator and zip64 end
213
     * of central directory record.
214
     *
215
     * zip64 end of central dir
216
     * signature                       4 bytes  (0x06064b50)
217
     * size of zip64 end of central
218
     * directory record                8 bytes
219
     * version made by                 2 bytes
220
     * version needed to extract       2 bytes
221
     * number of this disk             4 bytes
222
     * number of the disk with the
223
     * start of the central directory  4 bytes
224
     * total number of entries in the
225
     * central directory on this disk  8 bytes
226
     * total number of entries in the
227
     * central directory               8 bytes
228
     * size of the central directory   8 bytes
229
     * offset of start of central
230
     * directory with respect to
231
     * the starting disk number        8 bytes
232
     * zip64 extensible data sector    (variable size)
233
     *
234
     * @param int $zip64ECDPosition
235
     *
236
     * @throws ZipException
237
     *
238
     * @return EndOfCentralDirectory
239
     */
240
    protected function readZip64EndOfCentralDirectory($zip64ECDPosition)
241
    {
242
        fseek($this->in, $zip64ECDPosition);
243
244
        $buffer = fread($this->in, 56 /* zip64 end of cd rec length */);
245
246
        if (unpack('V', $buffer)[1] !== EndOfCentralDirectory::ZIP64_END_OF_CD_RECORD_SIG) {
247
            throw new ZipException('Expected ZIP64 End Of Central Directory Record!');
248
        }
249
250
        $data = unpack(
251
            'VdiskNo/VcdDiskNo',
252
            substr($buffer, 16)
253
        );
254
        $cdEntriesDisk = PackUtil::unpackLongLE(substr($buffer, 24, 8));
255
        $entryCount = PackUtil::unpackLongLE(substr($buffer, 32, 8));
256
        $cdSize = PackUtil::unpackLongLE(substr($buffer, 40, 8));
257
        $cdPos = PackUtil::unpackLongLE(substr($buffer, 48, 8));
258
259
        if ($data['diskNo'] !== 0 || $data['cdDiskNo'] !== 0 || $entryCount !== $cdEntriesDisk) {
260
            throw new ZipException('ZIP file spanning/splitting is not supported!');
261
        }
262
263
        if ($entryCount < 0 || $entryCount > 0x7fffffff) {
264
            throw new ZipException('Total Number Of Entries In The Central Directory out of range!');
265
        }
266
267
        // skip zip64 extensible data sector (variable sizeEndCD)
268
269
        return new EndOfCentralDirectory(
270
            $entryCount,
271
            $cdPos,
272
            $cdSize,
273
            true
274
        );
275
    }
276
277
    /**
278
     * Reads the central directory from the given seekable byte channel
279
     * and populates the internal tables with ZipEntry instances.
280
     *
281
     * The ZipEntry's will know all data that can be obtained from the
282
     * central directory alone, but not the data that requires the local
283
     * file header or additional data to be read.
284
     *
285
     * @param EndOfCentralDirectory $endOfCentralDirectory
286
     *
287
     * @throws ZipException
288
     *
289
     * @return ZipEntry[]
290
     */
291
    protected function mountCentralDirectory(EndOfCentralDirectory $endOfCentralDirectory)
292
    {
293
        $entries = [];
294
295
        fseek($this->in, $endOfCentralDirectory->getCdOffset());
296
297
        if (!($cdStream = fopen('php://temp', 'w+b'))) {
298
            throw new ZipException('Temp resource can not open from write');
299
        }
300
        stream_copy_to_stream($this->in, $cdStream, $endOfCentralDirectory->getCdSize());
301
        rewind($cdStream);
302
        for ($numEntries = $endOfCentralDirectory->getEntryCount(); $numEntries > 0; $numEntries--) {
303
            $entry = $this->readCentralDirectoryEntry($cdStream);
304
            $entries[$entry->getName()] = $entry;
305
        }
306
        fclose($cdStream);
307
308
        return $entries;
309
    }
310
311
    /**
312
     * Read central directory entry.
313
     *
314
     * central file header signature   4 bytes  (0x02014b50)
315
     * version made by                 2 bytes
316
     * version needed to extract       2 bytes
317
     * general purpose bit flag        2 bytes
318
     * compression method              2 bytes
319
     * last mod file time              2 bytes
320
     * last mod file date              2 bytes
321
     * crc-32                          4 bytes
322
     * compressed size                 4 bytes
323
     * uncompressed size               4 bytes
324
     * file name length                2 bytes
325
     * extra field length              2 bytes
326
     * file comment length             2 bytes
327
     * disk number start               2 bytes
328
     * internal file attributes        2 bytes
329
     * external file attributes        4 bytes
330
     * relative offset of local header 4 bytes
331
     *
332
     * file name (variable size)
333
     * extra field (variable size)
334
     * file comment (variable size)
335
     *
336
     * @param resource $stream
337
     *
338
     * @throws ZipException
339
     *
340
     * @return ZipEntry
341
     */
342
    public function readCentralDirectoryEntry($stream)
343
    {
344
        if (unpack('V', fread($stream, 4))[1] !== ZipOutputStreamInterface::CENTRAL_FILE_HEADER_SIG) {
345
            throw new ZipException('Corrupt zip file. Cannot read central dir entry.');
346
        }
347
348
        $data = unpack(
349
            'vversionMadeBy/vversionNeededToExtract/' .
350
            'vgeneralPurposeBitFlag/vcompressionMethod/' .
351
            'VlastModFile/Vcrc/VcompressedSize/' .
352
            'VuncompressedSize/vfileNameLength/vextraFieldLength/' .
353
            'vfileCommentLength/vdiskNumberStart/vinternalFileAttributes/' .
354
            'VexternalFileAttributes/VoffsetLocalHeader',
355
            fread($stream, 42)
356
        );
357
358
        $createdOS = ($data['versionMadeBy'] & 0xFF00) >> 8;
359
        $softwareVersion = $data['versionMadeBy'] & 0x00FF;
360
361
        $extractOS = ($data['versionNeededToExtract'] & 0xFF00) >> 8;
362
        $extractVersion = $data['versionNeededToExtract'] & 0x00FF;
363
364
        $name = fread($stream, $data['fileNameLength']);
365
366
        $extra = '';
367
368
        if ($data['extraFieldLength'] > 0) {
369
            $extra = fread($stream, $data['extraFieldLength']);
370
        }
371
372
        $comment = null;
373
374
        if ($data['fileCommentLength'] > 0) {
375
            $comment = fread($stream, $data['fileCommentLength']);
376
        }
377
378
        $entry = new ZipSourceEntry($this);
379
        $entry->setName($name);
380
        $entry->setCreatedOS($createdOS);
381
        $entry->setSoftwareVersion($softwareVersion);
382
        $entry->setVersionNeededToExtract($extractVersion);
383
        $entry->setExtractedOS($extractOS);
384
        $entry->setMethod($data['compressionMethod']);
385
        $entry->setGeneralPurposeBitFlags($data['generalPurposeBitFlag']);
386
        $entry->setDosTime($data['lastModFile']);
387
        $entry->setCrc($data['crc']);
388
        $entry->setCompressedSize($data['compressedSize']);
389
        $entry->setSize($data['uncompressedSize']);
390
        $entry->setInternalAttributes($data['internalFileAttributes']);
391
        $entry->setExternalAttributes($data['externalFileAttributes']);
392
        $entry->setOffset($data['offsetLocalHeader']);
393
        $entry->setComment($comment);
394
        $entry->setExtra($extra);
395
396
        return $entry;
397
    }
398
399
    /**
400
     * @param ZipEntry $entry
401
     *
402
     * @throws ZipException
403
     *
404
     * @return string
405
     */
406
    public function readEntryContent(ZipEntry $entry)
407
    {
408
        if ($entry->isDirectory()) {
409
            return null;
410
        }
411
412
        if (!($entry instanceof ZipSourceEntry)) {
413
            throw new InvalidArgumentException('entry must be ' . ZipSourceEntry::class);
414
        }
415
        $isEncrypted = $entry->isEncrypted();
416
417
        if ($isEncrypted && $entry->getPassword() === null) {
0 ignored issues
show
introduced by
The condition $entry->getPassword() === null is always false.
Loading history...
418
            throw new ZipException('Can not password from entry ' . $entry->getName());
419
        }
420
421
        $startPos = $pos = $entry->getOffset();
422
423
        fseek($this->in, $startPos);
424
425
        // local file header signature     4 bytes  (0x04034b50)
426
        if (unpack('V', fread($this->in, 4))[1] !== ZipEntry::LOCAL_FILE_HEADER_SIG) {
427
            throw new ZipException($entry->getName() . ' (expected Local File Header)');
428
        }
429
        fseek($this->in, $pos + ZipEntry::LOCAL_FILE_HEADER_FILE_NAME_LENGTH_POS);
430
        // file name length                2 bytes
431
        // extra field length              2 bytes
432
        $data = unpack('vfileLength/vextraLength', fread($this->in, 4));
433
        $pos += ZipEntry::LOCAL_FILE_HEADER_MIN_LEN + $data['fileLength'] + $data['extraLength'];
434
435
        if ($entry->getCrc() === ZipEntry::UNKNOWN) {
436
            throw new ZipException(sprintf('Missing crc for entry %s', $entry->getName()));
437
        }
438
439
        $method = $entry->getMethod();
440
441
        fseek($this->in, $pos);
442
443
        // Get raw entry content
444
        $compressedSize = $entry->getCompressedSize();
445
        $content = '';
446
447
        if ($compressedSize > 0) {
448
            $offset = 0;
449
450
            while ($offset < $compressedSize) {
451
                $read = min(8192 /* chunk size */, $compressedSize - $offset);
452
                $content .= fread($this->in, $read);
453
                $offset += $read;
454
            }
455
        }
456
457
        $skipCheckCrc = false;
458
459
        if ($isEncrypted) {
460
            if ($method === ZipEntry::METHOD_WINZIP_AES) {
461
                // Strong Encryption Specification - WinZip AES
462
                $winZipAesEngine = new WinZipAesEngine($entry);
463
                $content = $winZipAesEngine->decrypt($content);
464
                /**
465
                 * @var WinZipAesEntryExtraField $field
466
                 */
467
                $field = $entry->getExtraFieldsCollection()->get(WinZipAesEntryExtraField::getHeaderId());
468
                $method = $field->getMethod();
469
                $entry->setEncryptionMethod($field->getEncryptionMethod());
470
                $skipCheckCrc = true;
471
            } else {
472
                // Traditional PKWARE Decryption
473
                $zipCryptoEngine = new TraditionalPkwareEncryptionEngine($entry);
474
                $content = $zipCryptoEngine->decrypt($content);
475
                $entry->setEncryptionMethod(ZipFile::ENCRYPTION_METHOD_TRADITIONAL);
476
            }
477
478
            if (!$skipCheckCrc) {
479
                // Check CRC32 in the Local File Header or Data Descriptor.
480
                $localCrc = null;
481
482
                if ($entry->getGeneralPurposeBitFlag(ZipEntry::GPBF_DATA_DESCRIPTOR)) {
483
                    // The CRC32 is in the Data Descriptor after the compressed size.
484
                    // Note the Data Descriptor's Signature is optional:
485
                    // All newer apps should write it (and so does TrueVFS),
486
                    // but older apps might not.
487
                    fseek($this->in, $pos + $compressedSize);
488
                    $localCrc = unpack('V', fread($this->in, 4))[1];
489
490
                    if ($localCrc === ZipEntry::DATA_DESCRIPTOR_SIG) {
491
                        $localCrc = unpack('V', fread($this->in, 4))[1];
492
                    }
493
                } else {
494
                    fseek($this->in, $startPos + 14);
495
                    // The CRC32 in the Local File Header.
496
                    $localCrc = fread($this->in, 4)[1];
497
                }
498
499
                if (\PHP_INT_SIZE === 4) {
500
                    if (sprintf('%u', $entry->getCrc()) === sprintf('%u', $localCrc)) {
501
                        throw new Crc32Exception($entry->getName(), $entry->getCrc(), $localCrc);
502
                    }
503
                } elseif ($localCrc !== $entry->getCrc()) {
504
                    throw new Crc32Exception($entry->getName(), $entry->getCrc(), $localCrc);
505
                }
506
            }
507
        }
508
509
        switch ($method) {
510
            case ZipFile::METHOD_STORED:
511
                break;
512
513
            case ZipFile::METHOD_DEFLATED:
514
                /** @noinspection PhpUsageOfSilenceOperatorInspection */
515
                $content = @gzinflate($content);
516
                break;
517
518
            case ZipFile::METHOD_BZIP2:
519
                if (!\extension_loaded('bz2')) {
520
                    throw new ZipException('Extension bzip2 not install');
521
                }
522
                /** @noinspection PhpComposerExtensionStubsInspection */
523
                $content = bzdecompress($content);
524
525
                if (\is_int($content)) { // decompress error
526
                    $content = false;
527
                }
528
                break;
529
            default:
530
                throw new ZipUnsupportMethodException(
531
                    $entry->getName() .
532
                    ' (compression method ' . $method . ' is not supported)'
533
                );
534
        }
535
536
        if ($content === false) {
537
            if ($isEncrypted) {
538
                throw new ZipAuthenticationException(
539
                    sprintf(
540
                        'Invalid password for zip entry "%s"',
541
                        $entry->getName()
542
                    )
543
                );
544
            }
545
546
            throw new ZipException(
547
                sprintf(
548
                    'Failed to get the contents of the zip entry "%s"',
549
                    $entry->getName()
550
                )
551
            );
552
        }
553
554
        if (!$skipCheckCrc) {
555
            $localCrc = crc32($content);
556
557
            if (sprintf('%u', $entry->getCrc()) !== sprintf('%u', $localCrc)) {
558
                if ($isEncrypted) {
559
                    throw new ZipAuthenticationException(
560
                        sprintf(
561
                            'Invalid password for zip entry "%s"',
562
                            $entry->getName()
563
                        )
564
                    );
565
                }
566
567
                throw new Crc32Exception($entry->getName(), $entry->getCrc(), $localCrc);
568
            }
569
        }
570
571
        return $content;
572
    }
573
574
    /**
575
     * @return resource
576
     */
577
    public function getStream()
578
    {
579
        return $this->in;
580
    }
581
582
    /**
583
     * Copy the input stream of the LOC entry zip and the data into
584
     * the output stream and zip the alignment if necessary.
585
     *
586
     * @param ZipEntry                 $entry
587
     * @param ZipOutputStreamInterface $out
588
     *
589
     * @throws ZipException
590
     */
591
    public function copyEntry(ZipEntry $entry, ZipOutputStreamInterface $out)
592
    {
593
        $pos = $entry->getOffset();
594
595
        if ($pos === ZipEntry::UNKNOWN) {
596
            throw new ZipException(sprintf('Missing local header offset for entry %s', $entry->getName()));
597
        }
598
599
        $nameLength = \strlen($entry->getName());
600
601
        fseek($this->in, $pos + ZipEntry::LOCAL_FILE_HEADER_MIN_LEN - 2, \SEEK_SET);
602
        $sourceExtraLength = $destExtraLength = unpack('v', fread($this->in, 2))[1];
603
604
        if ($sourceExtraLength > 0) {
605
            // read Local File Header extra fields
606
            fseek($this->in, $pos + ZipEntry::LOCAL_FILE_HEADER_MIN_LEN + $nameLength, \SEEK_SET);
607
            $extra = '';
608
            $offset = 0;
609
610
            while ($offset < $sourceExtraLength) {
611
                $read = min(8192 /* chunk size */, $sourceExtraLength - $offset);
612
                $extra .= fread($this->in, $read);
613
                $offset += $read;
614
            }
615
            $extraFieldsCollection = ExtraFieldsFactory::createExtraFieldCollections($extra, $entry);
616
617
            if (isset($extraFieldsCollection[ApkAlignmentExtraField::getHeaderId()]) && $this->zipModel->isZipAlign()) {
618
                unset($extraFieldsCollection[ApkAlignmentExtraField::getHeaderId()]);
619
                $destExtraLength = \strlen(ExtraFieldsFactory::createSerializedData($extraFieldsCollection));
620
            }
621
        } else {
622
            $extraFieldsCollection = new ExtraFieldsCollection();
623
        }
624
625
        $dataAlignmentMultiple = $this->zipModel->getZipAlign();
626
        $copyInToOutLength = $entry->getCompressedSize();
627
628
        fseek($this->in, $pos, \SEEK_SET);
629
630
        if (
631
            $this->zipModel->isZipAlign() &&
632
            !$entry->isEncrypted() &&
633
            $entry->getMethod() === ZipFile::METHOD_STORED
634
        ) {
635
            if (StringUtil::endsWith($entry->getName(), '.so')) {
636
                $dataAlignmentMultiple = ApkAlignmentExtraField::ANDROID_COMMON_PAGE_ALIGNMENT_BYTES;
637
            }
638
639
            $dataMinStartOffset =
640
                ftell($out->getStream()) +
641
                ZipEntry::LOCAL_FILE_HEADER_MIN_LEN +
642
                $destExtraLength +
643
                $nameLength +
644
                ApkAlignmentExtraField::ALIGNMENT_ZIP_EXTRA_MIN_SIZE_BYTES;
645
            $padding =
646
                ($dataAlignmentMultiple - ($dataMinStartOffset % $dataAlignmentMultiple))
647
                % $dataAlignmentMultiple;
648
649
            $alignExtra = new ApkAlignmentExtraField();
650
            $alignExtra->setMultiple($dataAlignmentMultiple);
651
            $alignExtra->setPadding($padding);
652
            $extraFieldsCollection->add($alignExtra);
653
654
            $extra = ExtraFieldsFactory::createSerializedData($extraFieldsCollection);
655
656
            // copy Local File Header without extra field length
657
            // from input stream to output stream
658
            stream_copy_to_stream($this->in, $out->getStream(), ZipEntry::LOCAL_FILE_HEADER_MIN_LEN - 2);
659
            // write new extra field length (2 bytes) to output stream
660
            fwrite($out->getStream(), pack('v', \strlen($extra)));
661
            // skip 2 bytes to input stream
662
            fseek($this->in, 2, \SEEK_CUR);
663
            // copy name from input stream to output stream
664
            stream_copy_to_stream($this->in, $out->getStream(), $nameLength);
665
            // write extra field to output stream
666
            fwrite($out->getStream(), $extra);
667
            // skip source extraLength from input stream
668
            fseek($this->in, $sourceExtraLength, \SEEK_CUR);
669
        } else {
670
            $copyInToOutLength += ZipEntry::LOCAL_FILE_HEADER_MIN_LEN + $sourceExtraLength + $nameLength;
671
        }
672
673
        if ($entry->getGeneralPurposeBitFlag(ZipEntry::GPBF_DATA_DESCRIPTOR)) {
674
//            crc-32                          4 bytes
675
//            compressed size                 4 bytes
676
//            uncompressed size               4 bytes
677
            $copyInToOutLength += 12;
678
679
            if ($entry->isZip64ExtensionsRequired()) {
680
//              compressed size                 +4 bytes
681
//              uncompressed size               +4 bytes
682
                $copyInToOutLength += 8;
683
            }
684
        }
685
        // copy loc, data, data descriptor from input to output stream
686
        stream_copy_to_stream($this->in, $out->getStream(), $copyInToOutLength);
687
    }
688
689
    /**
690
     * @param ZipEntry                 $entry
691
     * @param ZipOutputStreamInterface $out
692
     */
693
    public function copyEntryData(ZipEntry $entry, ZipOutputStreamInterface $out)
694
    {
695
        $offset = $entry->getOffset();
696
        $nameLength = \strlen($entry->getName());
697
698
        fseek($this->in, $offset + ZipEntry::LOCAL_FILE_HEADER_MIN_LEN - 2, \SEEK_SET);
699
        $extraLength = unpack('v', fread($this->in, 2))[1];
700
701
        fseek($this->in, $offset + ZipEntry::LOCAL_FILE_HEADER_MIN_LEN + $nameLength + $extraLength, \SEEK_SET);
702
        // copy raw data from input stream to output stream
703
        stream_copy_to_stream($this->in, $out->getStream(), $entry->getCompressedSize());
704
    }
705
706
    public function __destruct()
707
    {
708
        $this->close();
709
    }
710
711
    public function close()
712
    {
713
        if ($this->in !== null) {
714
            fclose($this->in);
715
            $this->in = null;
716
        }
717
    }
718
}
719