Passed
Branch master (25d5dd)
by Alexey
02:30
created

ZipWriter::zipAlign()   B

Complexity

Conditions 7
Paths 9

Size

Total Lines 42
Code Lines 28

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 23
CRAP Score 7.025

Importance

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

357
                $contextFilter = $this->appendCompressionFilter(/** @scrutinizer ignore-type */ $compressStream, $entry);
Loading history...
358 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

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

365
                rewind(/** @scrutinizer ignore-type */ $compressStream);
Loading history...
366
367 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

367
                $compressedSize = fstat(/** @scrutinizer ignore-type */ $compressStream)['size'];
Loading history...
368 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...
369
370 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

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