ZipWriter::writeCentralDirectoryHeader()   F
last analyzed

Complexity

Conditions 14
Paths 324

Size

Total Lines 114
Code Lines 60

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 39
CRAP Score 18.2018

Importance

Changes 1
Bugs 0 Features 1
Metric Value
cc 14
eloc 60
nc 324
nop 2
dl 0
loc 114
ccs 39
cts 54
cp 0.7221
crap 18.2018
rs 3.8833
c 1
b 0
f 1

How to fix   Long Method    Complexity   

Long Method

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

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

Commonly applied refactorings include:

1
<?php
2
3
namespace PhpZip\IO;
4
5
use PhpZip\Constants\DosCodePage;
6
use PhpZip\Constants\ZipCompressionMethod;
7
use PhpZip\Constants\ZipConstants;
8
use PhpZip\Constants\ZipEncryptionMethod;
9
use PhpZip\Constants\ZipPlatform;
10
use PhpZip\Constants\ZipVersion;
11
use PhpZip\Exception\ZipException;
12
use PhpZip\Exception\ZipUnsupportMethodException;
13
use PhpZip\IO\Filter\Cipher\Pkware\PKEncryptionStreamFilter;
14
use PhpZip\IO\Filter\Cipher\WinZipAes\WinZipAesEncryptionStreamFilter;
15
use PhpZip\Model\Data\ZipSourceFileData;
16
use PhpZip\Model\Extra\Fields\ApkAlignmentExtraField;
17
use PhpZip\Model\Extra\Fields\WinZipAesExtraField;
18
use PhpZip\Model\Extra\Fields\Zip64ExtraField;
19
use PhpZip\Model\ZipContainer;
20
use PhpZip\Model\ZipEntry;
21
use PhpZip\Util\PackUtil;
22
use PhpZip\Util\StringUtil;
23
24
/**
25
 * Class ZipWriter.
26
 */
27
class ZipWriter
28
{
29
    /** @var int Chunk read size */
30
    const CHUNK_SIZE = 8192;
31
32
    /** @var ZipContainer */
33
    protected $zipContainer;
34
35
    /**
36
     * ZipWriter constructor.
37
     *
38
     * @param ZipContainer $container
39
     */
40 136
    public function __construct(ZipContainer $container)
41
    {
42
        // we clone the container so that the changes made to
43
        // it do not affect the data in the ZipFile class
44 136
        $this->zipContainer = clone $container;
45 136
    }
46
47
    /**
48
     * @param resource $outStream
49
     *
50
     * @throws ZipException
51
     */
52 136
    public function write($outStream)
53
    {
54 136
        if (!\is_resource($outStream)) {
55
            throw new \InvalidArgumentException('$outStream must be resource');
56
        }
57 136
        $this->beforeWrite();
58 136
        $this->writeLocalBlock($outStream);
59 136
        $cdOffset = ftell($outStream);
60 136
        $this->writeCentralDirectoryBlock($outStream);
61 136
        $cdSize = ftell($outStream) - $cdOffset;
62 136
        $this->writeEndOfCentralDirectoryBlock($outStream, $cdOffset, $cdSize);
63 136
    }
64
65 136
    protected function beforeWrite()
66
    {
67 136
    }
68
69
    /**
70
     * @param resource $outStream
71
     *
72
     * @throws ZipException
73
     */
74 136
    protected function writeLocalBlock($outStream)
75
    {
76 136
        $zipEntries = $this->zipContainer->getEntries();
77
78 136
        foreach ($zipEntries as $zipEntry) {
79 134
            $this->writeLocalHeader($outStream, $zipEntry);
80 134
            $this->writeData($outStream, $zipEntry);
81
82 134
            if ($zipEntry->isDataDescriptorEnabled()) {
83 4
                $this->writeDataDescriptor($outStream, $zipEntry);
84
            }
85
        }
86 136
    }
87
88
    /**
89
     * @param resource $outStream
90
     * @param ZipEntry $entry
91
     *
92
     * @throws ZipException
93
     */
94 134
    protected function writeLocalHeader($outStream, ZipEntry $entry)
95
    {
96
        // todo in 4.0 version move zipalign functional to ApkWriter class
97 134
        if ($this->zipContainer->isZipAlign()) {
98 4
            $this->zipAlign($outStream, $entry);
99
        }
100
101 134
        $relativeOffset = ftell($outStream);
102 134
        $entry->setLocalHeaderOffset($relativeOffset);
103
104 134
        if ($entry->isEncrypted() && $entry->getEncryptionMethod() === ZipEncryptionMethod::PKWARE) {
105 4
            $entry->enableDataDescriptor(true);
106
        }
107
108 134
        $dd = $entry->isDataDescriptorRequired() ||
109 134
            $entry->isDataDescriptorEnabled();
110
111 134
        $compressedSize = $entry->getCompressedSize();
112 134
        $uncompressedSize = $entry->getUncompressedSize();
113
114 134
        $entry->getLocalExtraFields()->remove(Zip64ExtraField::HEADER_ID);
115
116 134
        if ($compressedSize > ZipConstants::ZIP64_MAGIC || $uncompressedSize > ZipConstants::ZIP64_MAGIC) {
117
            $entry->getLocalExtraFields()->add(
118
                new Zip64ExtraField($uncompressedSize, $compressedSize)
119
            );
120
121
            $compressedSize = ZipConstants::ZIP64_MAGIC;
122
            $uncompressedSize = ZipConstants::ZIP64_MAGIC;
123
        }
124
125 134
        $compressionMethod = $entry->getCompressionMethod();
126 134
        $crc = $entry->getCrc();
127
128 134
        if ($entry->isEncrypted() && ZipEncryptionMethod::isWinZipAesMethod($entry->getEncryptionMethod())) {
129
            /** @var WinZipAesExtraField|null $winZipAesExtra */
130 9
            $winZipAesExtra = $entry->getLocalExtraField(WinZipAesExtraField::HEADER_ID);
131
132 9
            if ($winZipAesExtra === null) {
133 8
                $winZipAesExtra = WinZipAesExtraField::create($entry);
134
            }
135
136 9
            if ($winZipAesExtra->isV2()) {
137 7
                $crc = 0;
138
            }
139 9
            $compressionMethod = ZipCompressionMethod::WINZIP_AES;
140
        }
141
142 134
        $extra = $this->getExtraFieldsContents($entry, true);
143 134
        $name = $entry->getName();
144 134
        $dosCharset = $entry->getCharset();
145
146 134
        if ($dosCharset !== null && !$entry->isUtf8Flag()) {
147
            $name = DosCodePage::fromUTF8($name, $dosCharset);
148
        }
149
150 134
        $nameLength = \strlen($name);
151 134
        $extraLength = \strlen($extra);
152
153 134
        $size = $nameLength + $extraLength;
154
155 134
        if ($size > 0xffff) {
156
            throw new ZipException(
157
                sprintf(
158
                    '%s (the total size of %s bytes for the name, extra fields and comment exceeds the maximum size of %d bytes)',
159
                    $entry->getName(),
160
                    $size,
161
                    0xffff
162
                )
163
            );
164
        }
165
166 134
        $extractedBy = ($entry->getExtractedOS() << 8) | $entry->getExtractVersion();
167
168 134
        fwrite(
169 134
            $outStream,
170
            pack(
171 134
                'VvvvVVVVvv',
172
                // local file header signature     4 bytes  (0x04034b50)
173 134
                ZipConstants::LOCAL_FILE_HEADER,
174
                // version needed to extract       2 bytes
175
                $extractedBy,
176
                // general purpose bit flag        2 bytes
177 134
                $entry->getGeneralPurposeBitFlags(),
178
                // compression method              2 bytes
179
                $compressionMethod,
180
                // last mod file time              2 bytes
181
                // last mod file date              2 bytes
182 134
                $entry->getDosTime(),
183
                // crc-32                          4 bytes
184 134
                $dd ? 0 : $crc,
185
                // compressed size                 4 bytes
186 134
                $dd ? 0 : $compressedSize,
187
                // uncompressed size               4 bytes
188 134
                $dd ? 0 : $uncompressedSize,
189
                // file name length                2 bytes
190
                $nameLength,
191
                // extra field length              2 bytes
192
                $extraLength
193
            )
194
        );
195
196 134
        if ($nameLength > 0) {
197 134
            fwrite($outStream, $name);
198
        }
199
200 134
        if ($extraLength > 0) {
201 15
            fwrite($outStream, $extra);
202
        }
203 134
    }
204
205
    /**
206
     * @param resource $outStream
207
     * @param ZipEntry $entry
208
     *
209
     * @throws ZipException
210
     */
211 4
    private function zipAlign($outStream, ZipEntry $entry)
212
    {
213 4
        if (!$entry->isDirectory() && $entry->getCompressionMethod() === ZipCompressionMethod::STORED) {
214 4
            $entry->removeExtraField(ApkAlignmentExtraField::HEADER_ID);
215
216 4
            $extra = $this->getExtraFieldsContents($entry, true);
217 4
            $extraLength = \strlen($extra);
218 4
            $name = $entry->getName();
219
220 4
            $dosCharset = $entry->getCharset();
221
222 4
            if ($dosCharset !== null && !$entry->isUtf8Flag()) {
223
                $name = DosCodePage::fromUTF8($name, $dosCharset);
224
            }
225 4
            $nameLength = \strlen($name);
226
227 4
            $multiple = ApkAlignmentExtraField::ALIGNMENT_BYTES;
228
229 4
            if (StringUtil::endsWith($name, '.so')) {
230
                $multiple = ApkAlignmentExtraField::COMMON_PAGE_ALIGNMENT_BYTES;
231
            }
232
233 4
            $offset = ftell($outStream);
234
235
            $dataMinStartOffset =
236
                $offset +
237 4
                ZipConstants::LFH_FILENAME_POS +
238 4
                $extraLength +
239 4
                $nameLength;
240
241
            $padding =
242 4
                ($multiple - ($dataMinStartOffset % $multiple))
243 4
                % $multiple;
244
245 4
            if ($padding > 0) {
246 4
                $dataMinStartOffset += ApkAlignmentExtraField::MIN_SIZE;
247
                $padding =
248 4
                    ($multiple - ($dataMinStartOffset % $multiple))
249 4
                    % $multiple;
250
251 4
                $entry->getLocalExtraFields()->add(
252 4
                    new ApkAlignmentExtraField($multiple, $padding)
253
                );
254
            }
255
        }
256 4
    }
257
258
    /**
259
     * Merges the local file data fields of the given ZipExtraFields.
260
     *
261
     * @param ZipEntry $entry
262
     * @param bool     $local
263
     *
264
     * @throws ZipException
265
     *
266
     * @return string
267
     */
268 134
    protected function getExtraFieldsContents(ZipEntry $entry, $local)
269
    {
270 134
        $local = (bool) $local;
271 134
        $collection = $local ?
272 134
            $entry->getLocalExtraFields() :
273 134
            $entry->getCdExtraFields();
274 134
        $extraData = '';
275
276 134
        foreach ($collection as $extraField) {
277 15
            if ($local) {
278 15
                $data = $extraField->packLocalFileData();
279
            } else {
280 12
                $data = $extraField->packCentralDirData();
281
            }
282 15
            $extraData .= pack(
283 15
                'vv',
284 15
                $extraField->getHeaderId(),
285 15
                \strlen($data)
286
            );
287 15
            $extraData .= $data;
288
        }
289
290 134
        $size = \strlen($extraData);
291
292 134
        if ($size > 0xffff) {
293
            throw new ZipException(
294
                sprintf(
295
                    'Size extra out of range: %d. Extra data: %s',
296
                    $size,
297
                    $extraData
298
                )
299
            );
300
        }
301
302 134
        return $extraData;
303
    }
304
305
    /**
306
     * @param resource $outStream
307
     * @param ZipEntry $entry
308
     *
309
     * @throws ZipException
310
     */
311 134
    protected function writeData($outStream, ZipEntry $entry)
312
    {
313 134
        $zipData = $entry->getData();
314
315 134
        if ($zipData === null) {
316 23
            if ($entry->isDirectory()) {
317 23
                return;
318
            }
319
320
            throw new ZipException(sprintf('No zip data for entry "%s"', $entry->getName()));
321
        }
322
323
        // data write variants:
324
        // --------------------
325
        // * data of source zip file -> copy compressed data
326
        // * store - simple write
327
        // * store and encryption - apply encryption filter and simple write
328
        // * deflate or bzip2 - apply compression filter and simple write
329
        // * (deflate or bzip2) and encryption - create temp stream and apply
330
        //     compression filter to it, then apply encryption filter to root
331
        //     stream and write temp stream data.
332
        //     (PHP cannot apply the filter for encryption after the compression
333
        //     filter, so a temporary stream is created for the compressed data)
334
335 134
        if ($zipData instanceof ZipSourceFileData && !$zipData->hasRecompressData($entry)) {
336
            // data of source zip file -> copy compressed data
337 28
            $zipData->copyCompressedDataToStream($outStream);
338
339 28
            return;
340
        }
341
342 133
        $entryStream = $zipData->getDataAsStream();
343
344 133
        if (stream_get_meta_data($entryStream)['seekable']) {
345 132
            rewind($entryStream);
346
        }
347
348 133
        $uncompressedSize = $entry->getUncompressedSize();
349
350 133
        $posBeforeWrite = ftell($outStream);
351 133
        $compressionMethod = $entry->getCompressionMethod();
352
353 133
        if ($entry->isEncrypted()) {
354 10
            if ($compressionMethod === ZipCompressionMethod::STORED) {
355 7
                $contextFilter = $this->appendEncryptionFilter($outStream, $entry, $uncompressedSize);
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $contextFilter is correct as $this->appendEncryptionF...try, $uncompressedSize) targeting PhpZip\IO\ZipWriter::appendEncryptionFilter() seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
356 7
                $checksum = $this->writeAndCountChecksum($entryStream, $outStream, $uncompressedSize);
357
            } else {
358 3
                $compressStream = fopen('php://temp', 'w+b');
359 3
                $contextFilter = $this->appendCompressionFilter($compressStream, $entry);
0 ignored issues
show
Bug introduced by
It seems like $compressStream can also be of type false; however, parameter $outStream of PhpZip\IO\ZipWriter::appendCompressionFilter() 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

359
                $contextFilter = $this->appendCompressionFilter(/** @scrutinizer ignore-type */ $compressStream, $entry);
Loading history...
360 3
                $checksum = $this->writeAndCountChecksum($entryStream, $compressStream, $uncompressedSize);
0 ignored issues
show
Bug introduced by
It seems like $compressStream can also be of type false; however, parameter $outStream of PhpZip\IO\ZipWriter::writeAndCountChecksum() 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

360
                $checksum = $this->writeAndCountChecksum($entryStream, /** @scrutinizer ignore-type */ $compressStream, $uncompressedSize);
Loading history...
361
362 3
                if ($contextFilter !== null) {
363 3
                    stream_filter_remove($contextFilter);
364 3
                    $contextFilter = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $contextFilter is dead and can be removed.
Loading history...
365
                }
366
367 3
                rewind($compressStream);
0 ignored issues
show
Bug introduced by
It seems like $compressStream can also be of type false; however, parameter $handle of rewind() 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

367
                rewind(/** @scrutinizer ignore-type */ $compressStream);
Loading history...
368
369 3
                $compressedSize = fstat($compressStream)['size'];
0 ignored issues
show
Bug introduced by
It seems like $compressStream can also be of type false; however, parameter $handle of fstat() 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

369
                $compressedSize = fstat(/** @scrutinizer ignore-type */ $compressStream)['size'];
Loading history...
370 3
                $contextFilter = $this->appendEncryptionFilter($outStream, $entry, $compressedSize);
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $contextFilter is correct as $this->appendEncryptionF...entry, $compressedSize) targeting PhpZip\IO\ZipWriter::appendEncryptionFilter() seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
371
372 10
                stream_copy_to_stream($compressStream, $outStream);
0 ignored issues
show
Bug introduced by
It seems like $compressStream can also be of type false; however, parameter $source of stream_copy_to_stream() 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

372
                stream_copy_to_stream(/** @scrutinizer ignore-type */ $compressStream, $outStream);
Loading history...
373
            }
374
        } else {
375 126
            $contextFilter = $this->appendCompressionFilter($outStream, $entry);
376 126
            $checksum = $this->writeAndCountChecksum($entryStream, $outStream, $uncompressedSize);
377
        }
378
379 133
        if ($contextFilter !== null) {
380 56
            stream_filter_remove($contextFilter);
381 56
            $contextFilter = null;
382
        }
383
384
        // my hack {@see https://bugs.php.net/bug.php?id=49874}
385 133
        fseek($outStream, 0, \SEEK_END);
386 133
        $compressedSize = ftell($outStream) - $posBeforeWrite;
387
388 133
        $entry->setCompressedSize($compressedSize);
389 133
        $entry->setCrc($checksum);
390
391 133
        if (!$entry->isDataDescriptorEnabled()) {
392 132
            if ($uncompressedSize > ZipConstants::ZIP64_MAGIC || $compressedSize > ZipConstants::ZIP64_MAGIC) {
393
                /** @var Zip64ExtraField|null $zip64ExtraLocal */
394
                $zip64ExtraLocal = $entry->getLocalExtraField(Zip64ExtraField::HEADER_ID);
395
396
                // if there is a zip64 extra record, then update it;
397
                // if not, write data to data descriptor
398
                if ($zip64ExtraLocal !== null) {
399
                    $zip64ExtraLocal->setCompressedSize($compressedSize);
400
                    $zip64ExtraLocal->setUncompressedSize($uncompressedSize);
401
402
                    $posExtra = $entry->getLocalHeaderOffset() + ZipConstants::LFH_FILENAME_POS + \strlen($entry->getName());
403
                    fseek($outStream, $posExtra);
404
                    fwrite($outStream, $this->getExtraFieldsContents($entry, true));
405
                } else {
406
                    $posGPBF = $entry->getLocalHeaderOffset() + 6;
407
                    $entry->enableDataDescriptor(true);
408
                    fseek($outStream, $posGPBF);
409
                    fwrite(
410
                        $outStream,
411
                        pack(
412
                            'v',
413
                            // general purpose bit flag        2 bytes
414
                            $entry->getGeneralPurposeBitFlags()
415
                        )
416
                    );
417
                }
418
419
                $compressedSize = ZipConstants::ZIP64_MAGIC;
420
                $uncompressedSize = ZipConstants::ZIP64_MAGIC;
421
            }
422
423 132
            $posChecksum = $entry->getLocalHeaderOffset() + 14;
424
425
            /** @var WinZipAesExtraField|null $winZipAesExtra */
426 132
            $winZipAesExtra = $entry->getLocalExtraField(WinZipAesExtraField::HEADER_ID);
427
428 132
            if ($winZipAesExtra !== null && $winZipAesExtra->isV2()) {
429 7
                $checksum = 0;
430
            }
431
432 132
            fseek($outStream, $posChecksum);
433 132
            fwrite(
434 132
                $outStream,
435
                pack(
436 132
                    'VVV',
437
                    // crc-32                          4 bytes
438
                    $checksum,
439
                    // compressed size                 4 bytes
440
                    $compressedSize,
441
                    // uncompressed size               4 bytes
442
                    $uncompressedSize
443
                )
444
            );
445 132
            fseek($outStream, 0, \SEEK_END);
446
        }
447 133
    }
448
449
    /**
450
     * @param resource $inStream
451
     * @param resource $outStream
452
     * @param int      $size
453
     *
454
     * @return int
455
     */
456 133
    private function writeAndCountChecksum($inStream, $outStream, $size)
457
    {
458 133
        $contextHash = hash_init('crc32b');
459 133
        $offset = 0;
460
461 133
        while ($offset < $size) {
462 128
            $read = min(self::CHUNK_SIZE, $size - $offset);
463 128
            $buffer = fread($inStream, $read);
464 128
            fwrite($outStream, $buffer);
465 128
            hash_update($contextHash, $buffer);
466 128
            $offset += $read;
467
        }
468
469 133
        return (int) hexdec(hash_final($contextHash));
470
    }
471
472
    /**
473
     * @param resource $outStream
474
     * @param ZipEntry $entry
475
     *
476
     * @throws ZipUnsupportMethodException
477
     *
478
     * @return resource|null
479
     */
480 128
    protected function appendCompressionFilter($outStream, ZipEntry $entry)
481
    {
482 128
        $contextCompress = null;
483 128
        switch ($entry->getCompressionMethod()) {
484
            case ZipCompressionMethod::DEFLATED:
485 49
                if (!($contextCompress = stream_filter_append(
486 49
                    $outStream,
487 49
                    'zlib.deflate',
488 49
                    \STREAM_FILTER_WRITE,
489 49
                    ['level' => $entry->getCompressionLevel()]
490
                ))) {
491
                    throw new \RuntimeException('Could not append filter "zlib.deflate" to out stream');
492
                }
493 49
                break;
494
495
            case ZipCompressionMethod::BZIP2:
496 4
                if (!($contextCompress = stream_filter_append(
497 4
                    $outStream,
498 4
                    'bzip2.compress',
499 4
                    \STREAM_FILTER_WRITE,
500 4
                    ['blocks' => $entry->getCompressionLevel(), 'work' => 0]
501
                ))) {
502
                    throw new \RuntimeException('Could not append filter "bzip2.compress" to out stream');
503
                }
504 4
                break;
505
506
            case ZipCompressionMethod::STORED:
507
                // file without compression, do nothing
508 107
                break;
509
510
            default:
511
                throw new ZipUnsupportMethodException(
512
                    sprintf(
513
                        '%s (compression method %d (%s) is not supported)',
514
                        $entry->getName(),
515
                        $entry->getCompressionMethod(),
516
                        ZipCompressionMethod::getCompressionMethodName($entry->getCompressionMethod())
517
                    )
518
                );
519
        }
520
521 128
        return $contextCompress;
522
    }
523
524
    /**
525
     * @param resource $outStream
526
     * @param ZipEntry $entry
527
     * @param int      $size
528
     *
529
     * @return resource|null
530
     */
531 10
    protected function appendEncryptionFilter($outStream, ZipEntry $entry, $size)
532
    {
533 10
        $encContextFilter = null;
534
535 10
        if ($entry->isEncrypted()) {
536 10
            if ($entry->getEncryptionMethod() === ZipEncryptionMethod::PKWARE) {
537 4
                PKEncryptionStreamFilter::register();
538 4
                $cipherFilterName = PKEncryptionStreamFilter::FILTER_NAME;
539
            } else {
540 9
                WinZipAesEncryptionStreamFilter::register();
541 9
                $cipherFilterName = WinZipAesEncryptionStreamFilter::FILTER_NAME;
542
            }
543 10
            $encContextFilter = stream_filter_append(
544 10
                $outStream,
545
                $cipherFilterName,
546 10
                \STREAM_FILTER_WRITE,
547
                [
548 10
                    'entry' => $entry,
549 10
                    'size' => $size,
550
                ]
551
            );
552
553 10
            if (!$encContextFilter) {
0 ignored issues
show
introduced by
$encContextFilter is of type resource, thus it always evaluated to false.
Loading history...
554
                throw new \RuntimeException('Not apply filter ' . $cipherFilterName);
555
            }
556
        }
557
558 10
        return $encContextFilter;
559
    }
560
561
    /**
562
     * @param resource $outStream
563
     * @param ZipEntry $entry
564
     */
565 4
    protected function writeDataDescriptor($outStream, ZipEntry $entry)
566
    {
567 4
        $crc = $entry->getCrc();
568
569
        /** @var WinZipAesExtraField|null $winZipAesExtra */
570 4
        $winZipAesExtra = $entry->getLocalExtraField(WinZipAesExtraField::HEADER_ID);
571
572 4
        if ($winZipAesExtra !== null && $winZipAesExtra->isV2()) {
573
            $crc = 0;
574
        }
575
576 4
        fwrite(
577 4
            $outStream,
578
            pack(
579 4
                'VV',
580
                // data descriptor signature       4 bytes  (0x08074b50)
581 4
                ZipConstants::DATA_DESCRIPTOR,
582
                // crc-32                          4 bytes
583
                $crc
584
            )
585
        );
586
587
        if (
588 4
            $entry->isZip64ExtensionsRequired() ||
589 4
            $entry->getLocalExtraFields()->has(Zip64ExtraField::HEADER_ID)
590
        ) {
591
            $dd =
592
                // compressed size                 8 bytes
593
                PackUtil::packLongLE($entry->getCompressedSize()) .
594
                // uncompressed size               8 bytes
595
                PackUtil::packLongLE($entry->getUncompressedSize());
596
        } else {
597 4
            $dd = pack(
598 4
                'VV',
599
                // compressed size                 4 bytes
600 4
                $entry->getCompressedSize(),
601
                // uncompressed size               4 bytes
602 4
                $entry->getUncompressedSize()
603
            );
604
        }
605
606 4
        fwrite($outStream, $dd);
607 4
    }
608
609
    /**
610
     * @param resource $outStream
611
     *
612
     * @throws ZipException
613
     */
614 136
    protected function writeCentralDirectoryBlock($outStream)
615
    {
616 136
        foreach ($this->zipContainer->getEntries() as $outputEntry) {
617 134
            $this->writeCentralDirectoryHeader($outStream, $outputEntry);
618
        }
619 136
    }
620
621
    /**
622
     * Writes a Central File Header record.
623
     *
624
     * @param resource $outStream
625
     * @param ZipEntry $entry
626
     *
627
     * @throws ZipException
628
     */
629 134
    protected function writeCentralDirectoryHeader($outStream, ZipEntry $entry)
630
    {
631 134
        $compressedSize = $entry->getCompressedSize();
632 134
        $uncompressedSize = $entry->getUncompressedSize();
633 134
        $localHeaderOffset = $entry->getLocalHeaderOffset();
634
635 134
        $entry->getCdExtraFields()->remove(Zip64ExtraField::HEADER_ID);
636
637
        if (
638 134
            $localHeaderOffset > ZipConstants::ZIP64_MAGIC ||
639 134
            $compressedSize > ZipConstants::ZIP64_MAGIC ||
640 134
            $uncompressedSize > ZipConstants::ZIP64_MAGIC
641
        ) {
642
            $zip64ExtraField = new Zip64ExtraField();
643
644
            if ($uncompressedSize >= ZipConstants::ZIP64_MAGIC) {
645
                $zip64ExtraField->setUncompressedSize($uncompressedSize);
646
                $uncompressedSize = ZipConstants::ZIP64_MAGIC;
647
            }
648
649
            if ($compressedSize >= ZipConstants::ZIP64_MAGIC) {
650
                $zip64ExtraField->setCompressedSize($compressedSize);
651
                $compressedSize = ZipConstants::ZIP64_MAGIC;
652
            }
653
654
            if ($localHeaderOffset >= ZipConstants::ZIP64_MAGIC) {
655
                $zip64ExtraField->setLocalHeaderOffset($localHeaderOffset);
656
                $localHeaderOffset = ZipConstants::ZIP64_MAGIC;
657
            }
658
659
            $entry->getCdExtraFields()->add($zip64ExtraField);
660
        }
661
662 134
        $extra = $this->getExtraFieldsContents($entry, false);
663 134
        $extraLength = \strlen($extra);
664
665 134
        $name = $entry->getName();
666 134
        $comment = $entry->getComment();
667
668 134
        $dosCharset = $entry->getCharset();
669
670 134
        if ($dosCharset !== null && !$entry->isUtf8Flag()) {
671
            $name = DosCodePage::fromUTF8($name, $dosCharset);
672
673
            if ($comment) {
674
                $comment = DosCodePage::fromUTF8($comment, $dosCharset);
675
            }
676
        }
677
678 134
        $commentLength = \strlen($comment);
679
680 134
        $compressionMethod = $entry->getCompressionMethod();
681 134
        $crc = $entry->getCrc();
682
683
        /** @var WinZipAesExtraField|null $winZipAesExtra */
684 134
        $winZipAesExtra = $entry->getLocalExtraField(WinZipAesExtraField::HEADER_ID);
685
686 134
        if ($winZipAesExtra !== null) {
687 9
            if ($winZipAesExtra->isV2()) {
688 7
                $crc = 0;
689
            }
690 9
            $compressionMethod = ZipCompressionMethod::WINZIP_AES;
691
        }
692
693 134
        fwrite(
694 134
            $outStream,
695
            pack(
696 134
                'VvvvvVVVVvvvvvVV',
697
                // central file header signature   4 bytes  (0x02014b50)
698 134
                ZipConstants::CENTRAL_FILE_HEADER,
699
                // version made by                 2 bytes
700 134
                ($entry->getCreatedOS() << 8) | $entry->getSoftwareVersion(),
701
                // version needed to extract       2 bytes
702 134
                ($entry->getExtractedOS() << 8) | $entry->getExtractVersion(),
703
                // general purpose bit flag        2 bytes
704 134
                $entry->getGeneralPurposeBitFlags(),
705
                // compression method              2 bytes
706
                $compressionMethod,
707
                // last mod file datetime          4 bytes
708 134
                $entry->getDosTime(),
709
                // crc-32                          4 bytes
710
                $crc,
711
                // compressed size                 4 bytes
712
                $compressedSize,
713
                // uncompressed size               4 bytes
714
                $uncompressedSize,
715
                // file name length                2 bytes
716 134
                \strlen($name),
717
                // extra field length              2 bytes
718
                $extraLength,
719
                // file comment length             2 bytes
720
                $commentLength,
721
                // disk number start               2 bytes
722 134
                0,
723
                // internal file attributes        2 bytes
724 134
                $entry->getInternalAttributes(),
725
                // external file attributes        4 bytes
726 134
                $entry->getExternalAttributes(),
727
                // relative offset of local header 4 bytes
728
                $localHeaderOffset
729
            )
730
        );
731
732
        // file name (variable size)
733 134
        fwrite($outStream, $name);
734
735 134
        if ($extraLength > 0) {
736
            // extra field (variable size)
737 12
            fwrite($outStream, $extra);
738
        }
739
740 134
        if ($commentLength > 0) {
741
            // file comment (variable size)
742 3
            fwrite($outStream, $comment);
743
        }
744 134
    }
745
746
    /**
747
     * @param resource $outStream
748
     * @param int      $centralDirectoryOffset
749
     * @param int      $centralDirectorySize
750
     */
751 136
    protected function writeEndOfCentralDirectoryBlock(
752
        $outStream,
753
        $centralDirectoryOffset,
754
        $centralDirectorySize
755
    ) {
756 136
        $cdEntriesCount = \count($this->zipContainer);
757
758 136
        $cdEntriesZip64 = $cdEntriesCount > 0xffff;
759 136
        $cdSizeZip64 = $centralDirectorySize > ZipConstants::ZIP64_MAGIC;
760 136
        $cdOffsetZip64 = $centralDirectoryOffset > ZipConstants::ZIP64_MAGIC;
761
762 136
        $zip64Required = $cdEntriesZip64
763 135
            || $cdSizeZip64
764 136
            || $cdOffsetZip64;
765
766 136
        if ($zip64Required) {
767 1
            $zip64EndOfCentralDirectoryOffset = ftell($outStream);
768
769
            // find max software version, version needed to extract and most common platform
770 1
            list($softwareVersion, $versionNeededToExtract) = array_reduce(
771 1
                $this->zipContainer->getEntries(),
772 1
                static function (array $carry, ZipEntry $entry) {
773 1
                    $carry[0] = max($carry[0], $entry->getSoftwareVersion() & 0xFF);
774 1
                    $carry[1] = max($carry[1], $entry->getExtractVersion() & 0xFF);
775
776 1
                    return $carry;
777 1
                },
778
                [ZipVersion::v10_DEFAULT_MIN, ZipVersion::v45_ZIP64_EXT]
779
            );
780
781
            $createdOS = $extractedOS = ZipPlatform::OS_DOS;
782
            $versionMadeBy = ($createdOS << 8) | max($softwareVersion, ZipVersion::v45_ZIP64_EXT);
783
            $versionExtractedBy = ($extractedOS << 8) | max($versionNeededToExtract, ZipVersion::v45_ZIP64_EXT);
784
785
            // write zip64 end of central directory signature
786
            fwrite(
787
                $outStream,
788
                pack(
789
                    'V',
790
                    // signature                       4 bytes  (0x06064b50)
791
                    ZipConstants::ZIP64_END_CD
792
                )
793
            );
794
            // size of zip64 end of central
795
            // directory record                8 bytes
796
            fwrite($outStream, PackUtil::packLongLE(ZipConstants::ZIP64_END_OF_CD_LEN - 12));
797
            fwrite(
798
                $outStream,
799
                pack(
800
                    'vvVV',
801
                    // version made by                 2 bytes
802
                    $versionMadeBy & 0xFFFF,
803
                    // version needed to extract       2 bytes
804
                    $versionExtractedBy & 0xFFFF,
805
                    // number of this disk             4 bytes
806
                    0,
807
                    // number of the disk with the
808
                    // start of the central directory  4 bytes
809
                    0
810
                )
811
            );
812
813
            fwrite(
814
                $outStream,
815
                // total number of entries in the
816
                // central directory on this disk  8 bytes
817
                PackUtil::packLongLE($cdEntriesCount) .
818
                // total number of entries in the
819
                // central directory               8 bytes
820
                PackUtil::packLongLE($cdEntriesCount) .
821
                // size of the central directory   8 bytes
822
                PackUtil::packLongLE($centralDirectorySize) .
823
                // offset of start of central
824
                // directory with respect to
825
                // the starting disk number        8 bytes
826
                PackUtil::packLongLE($centralDirectoryOffset)
827
            );
828
829
            // write zip64 end of central directory locator
830
            fwrite(
831
                $outStream,
832
                pack(
833
                    'VV',
834
                    // zip64 end of central dir locator
835
                    // signature                       4 bytes  (0x07064b50)
836
                    ZipConstants::ZIP64_END_CD_LOC,
837
                    // number of the disk with the
838
                    // start of the zip64 end of
839
                    // central directory               4 bytes
840
                    0
841
                ) .
842
                // relative offset of the zip64
843
                // end of central directory record 8 bytes
844
                PackUtil::packLongLE($zip64EndOfCentralDirectoryOffset) .
845
                // total number of disks           4 bytes
846
                pack('V', 1)
847
            );
848
        }
849
850
        $comment = $this->zipContainer->getArchiveComment();
851
        $commentLength = $comment !== null ? \strlen($comment) : 0;
852
853
        fwrite(
854
            $outStream,
855
            pack(
856
                'VvvvvVVv',
857
                // end of central dir signature    4 bytes  (0x06054b50)
858
                ZipConstants::END_CD,
859
                // number of this disk             2 bytes
860
                0,
861
                // number of the disk with the
862
                // start of the central directory  2 bytes
863
                0,
864
                // total number of entries in the
865
                // central directory on this disk  2 bytes
866
                $cdEntriesZip64 ? 0xffff : $cdEntriesCount,
867
                // total number of entries in
868
                // the central directory           2 bytes
869
                $cdEntriesZip64 ? 0xffff : $cdEntriesCount,
870
                // size of the central directory   4 bytes
871
                $cdSizeZip64 ? ZipConstants::ZIP64_MAGIC : $centralDirectorySize,
872
                // offset of start of central
873
                // directory with respect to
874
                // the starting disk number        4 bytes
875
                $cdOffsetZip64 ? ZipConstants::ZIP64_MAGIC : $centralDirectoryOffset,
876
                // .ZIP file comment length        2 bytes
877
                $commentLength
878
            )
879
        );
880
881
        if ($comment !== null && $commentLength > 0) {
882
            // .ZIP file comment       (variable size)
883
            fwrite($outStream, $comment);
884
        }
885
    }
886
}
887