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

ZipWriter::writeAndCountChecksum()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 14
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
cc 2
eloc 9
c 1
b 0
f 1
nc 2
nop 3
dl 0
loc 14
rs 9.9666
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
    public function __construct(ZipContainer $container)
41
    {
42
        $this->zipContainer = $container;
43
    }
44
45
    /**
46
     * @param resource $outStream
47
     *
48
     * @throws ZipException
49
     */
50
    public function write($outStream)
51
    {
52
        if (!\is_resource($outStream)) {
53
            throw new \InvalidArgumentException('$outStream must be resource');
54
        }
55
        $this->beforeWrite();
56
        $this->writeLocalBlock($outStream);
57
        $cdOffset = ftell($outStream);
58
        $this->writeCentralDirectoryBlock($outStream);
59
        $cdSize = ftell($outStream) - $cdOffset;
60
        $this->writeEndOfCentralDirectoryBlock($outStream, $cdOffset, $cdSize);
61
    }
62
63
    protected function beforeWrite()
64
    {
65
    }
66
67
    /**
68
     * @param resource $outStream
69
     *
70
     * @throws ZipException
71
     */
72
    protected function writeLocalBlock($outStream)
73
    {
74
        $zipEntries = $this->zipContainer->getEntries();
75
76
        foreach ($zipEntries as $zipEntry) {
77
            $this->writeLocalHeader($outStream, $zipEntry);
78
            $this->writeData($outStream, $zipEntry);
79
80
            if ($zipEntry->isDataDescriptorEnabled()) {
81
                $this->writeDataDescriptor($outStream, $zipEntry);
82
            }
83
        }
84
    }
85
86
    /**
87
     * @param resource $outStream
88
     * @param ZipEntry $entry
89
     *
90
     * @throws ZipException
91
     */
92
    protected function writeLocalHeader($outStream, ZipEntry $entry)
93
    {
94
        // todo in 4.0 version move zipalign functional to ApkWriter class
95
        if ($this->zipContainer->isZipAlign()) {
96
            $this->zipAlign($outStream, $entry);
97
        }
98
99
        $relativeOffset = ftell($outStream);
100
        $entry->setLocalHeaderOffset($relativeOffset);
101
102
        if ($entry->isEncrypted() && $entry->getEncryptionMethod() === ZipEncryptionMethod::PKWARE) {
103
            $entry->enableDataDescriptor(true);
104
        }
105
106
        $dd = $entry->isDataDescriptorRequired() ||
107
            $entry->isDataDescriptorEnabled();
108
109
        $compressedSize = $entry->getCompressedSize();
110
        $uncompressedSize = $entry->getUncompressedSize();
111
112
        $entry->getLocalExtraFields()->remove(Zip64ExtraField::HEADER_ID);
113
114
        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
        $compressionMethod = $entry->getCompressionMethod();
124
        $crc = $entry->getCrc();
125
126
        if ($entry->isEncrypted() && ZipEncryptionMethod::isWinZipAesMethod($entry->getEncryptionMethod())) {
127
            /** @var WinZipAesExtraField|null $winZipAesExtra */
128
            $winZipAesExtra = $entry->getLocalExtraField(WinZipAesExtraField::HEADER_ID);
129
130
            if ($winZipAesExtra === null) {
131
                $winZipAesExtra = WinZipAesExtraField::create($entry);
132
            }
133
134
            if ($winZipAesExtra->isV2()) {
135
                $crc = 0;
136
            }
137
            $compressionMethod = ZipCompressionMethod::WINZIP_AES;
138
        }
139
140
        $extra = $this->getExtraFieldsContents($entry, true);
141
        $name = $entry->getName();
142
        $dosCharset = $entry->getCharset();
143
144
        if ($dosCharset !== null && !$entry->isUtf8Flag()) {
145
            $name = DosCodePage::fromUTF8($name, $dosCharset);
146
        }
147
148
        $nameLength = \strlen($name);
149
        $extraLength = \strlen($extra);
150
151
        $size = $nameLength + $extraLength;
152
153
        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
        $extractedBy = ($entry->getExtractedOS() << 8) | $entry->getExtractVersion();
165
166
        fwrite(
167
            $outStream,
168
            pack(
169
                'VvvvVVVVvv',
170
                // local file header signature     4 bytes  (0x04034b50)
171
                ZipConstants::LOCAL_FILE_HEADER,
172
                // version needed to extract       2 bytes
173
                $extractedBy,
174
                // general purpose bit flag        2 bytes
175
                $entry->getGeneralPurposeBitFlags(),
176
                // compression method              2 bytes
177
                $compressionMethod,
178
                // last mod file time              2 bytes
179
                // last mod file date              2 bytes
180
                $entry->getDosTime(),
181
                // crc-32                          4 bytes
182
                $dd ? 0 : $crc,
183
                // compressed size                 4 bytes
184
                $dd ? 0 : $compressedSize,
185
                // uncompressed size               4 bytes
186
                $dd ? 0 : $uncompressedSize,
187
                // file name length                2 bytes
188
                $nameLength,
189
                // extra field length              2 bytes
190
                $extraLength
191
            )
192
        );
193
194
        if ($nameLength > 0) {
195
            fwrite($outStream, $name);
196
        }
197
198
        if ($extraLength > 0) {
199
            fwrite($outStream, $extra);
200
        }
201
    }
202
203
    /**
204
     * @param resource $outStream
205
     * @param ZipEntry $entry
206
     *
207
     * @throws ZipException
208
     */
209
    private function zipAlign($outStream, ZipEntry $entry)
210
    {
211
        if (!$entry->isDirectory() && $entry->getCompressionMethod() === ZipCompressionMethod::STORED) {
212
            $entry->removeExtraField(ApkAlignmentExtraField::HEADER_ID);
213
214
            $extra = $this->getExtraFieldsContents($entry, true);
215
            $extraLength = \strlen($extra);
216
            $name = $entry->getName();
217
218
            $dosCharset = $entry->getCharset();
219
220
            if ($dosCharset !== null && !$entry->isUtf8Flag()) {
221
                $name = DosCodePage::fromUTF8($name, $dosCharset);
222
            }
223
            $nameLength = \strlen($name);
224
225
            $multiple = ApkAlignmentExtraField::ALIGNMENT_BYTES;
226
227
            if (StringUtil::endsWith($name, '.so')) {
228
                $multiple = ApkAlignmentExtraField::COMMON_PAGE_ALIGNMENT_BYTES;
229
            }
230
231
            $offset = ftell($outStream);
232
233
            $dataMinStartOffset =
234
                $offset +
235
                ZipConstants::LFH_FILENAME_POS +
236
                $extraLength +
237
                $nameLength;
238
239
            $padding =
240
                ($multiple - ($dataMinStartOffset % $multiple))
241
                % $multiple;
242
243
            if ($padding > 0) {
244
                $dataMinStartOffset += ApkAlignmentExtraField::MIN_SIZE;
245
                $padding =
246
                    ($multiple - ($dataMinStartOffset % $multiple))
247
                    % $multiple;
248
249
                $entry->getLocalExtraFields()->add(
250
                    new ApkAlignmentExtraField($multiple, $padding)
251
                );
252
            }
253
        }
254
    }
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
    protected function getExtraFieldsContents(ZipEntry $entry, $local)
267
    {
268
        $local = (bool) $local;
269
        $collection = $local ?
270
            $entry->getLocalExtraFields() :
271
            $entry->getCdExtraFields();
272
        $extraData = '';
273
274
        foreach ($collection as $extraField) {
275
            if ($local) {
276
                $data = $extraField->packLocalFileData();
277
            } else {
278
                $data = $extraField->packCentralDirData();
279
            }
280
            $extraData .= pack(
281
                'vv',
282
                $extraField->getHeaderId(),
283
                \strlen($data)
284
            );
285
            $extraData .= $data;
286
        }
287
288
        $size = \strlen($extraData);
289
290
        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
        return $extraData;
301
    }
302
303
    /**
304
     * @param resource $outStream
305
     * @param ZipEntry $entry
306
     *
307
     * @throws ZipException
308
     */
309
    protected function writeData($outStream, ZipEntry $entry)
310
    {
311
        $zipData = $entry->getData();
312
313
        if ($zipData === null) {
314
            if ($entry->isDirectory()) {
315
                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
        if ($zipData instanceof ZipSourceFileData && !$zipData->hasRecompressData($entry)) {
334
            // data of source zip file -> copy compressed data
335
            $zipData->copyCompressedDataToStream($outStream);
336
337
            return;
338
        }
339
340
        $entryStream = $zipData->getDataAsStream();
341
342
        if (stream_get_meta_data($entryStream)['seekable']) {
343
            rewind($entryStream);
344
        }
345
346
        $uncompressedSize = $entry->getUncompressedSize();
347
348
        $posBeforeWrite = ftell($outStream);
349
        $compressionMethod = $entry->getCompressionMethod();
350
351
        if ($entry->isEncrypted()) {
352
            if ($compressionMethod === ZipCompressionMethod::STORED) {
353
                $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
                $checksum = $this->writeAndCountChecksum($entryStream, $outStream, $uncompressedSize);
355
            } else {
356
                $compressStream = fopen('php://temp', 'w+b');
357
                $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
                $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
                if ($contextFilter !== null) {
361
                    stream_filter_remove($contextFilter);
362
                    $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
                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
                $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
                $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
                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
            $contextFilter = $this->appendCompressionFilter($outStream, $entry);
374
            $checksum = $this->writeAndCountChecksum($entryStream, $outStream, $uncompressedSize);
375
        }
376
377
        if ($contextFilter !== null) {
378
            stream_filter_remove($contextFilter);
379
            $contextFilter = null;
380
        }
381
382
        // my hack {@see https://bugs.php.net/bug.php?id=49874}
383
        fseek($outStream, 0, \SEEK_END);
384
        $compressedSize = ftell($outStream) - $posBeforeWrite;
385
386
        $entry->setCompressedSize($compressedSize);
387
        $entry->setCrc($checksum);
388
389
        if (!$entry->isDataDescriptorEnabled()) {
390
            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
            $posChecksum = $entry->getLocalHeaderOffset() + 14;
422
423
            /** @var WinZipAesExtraField|null $winZipAesExtra */
424
            $winZipAesExtra = $entry->getLocalExtraField(WinZipAesExtraField::HEADER_ID);
425
426
            if ($winZipAesExtra !== null && $winZipAesExtra->isV2()) {
427
                $checksum = 0;
428
            }
429
430
            fseek($outStream, $posChecksum);
431
            fwrite(
432
                $outStream,
433
                pack(
434
                    '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
            fseek($outStream, 0, \SEEK_END);
444
        }
445
    }
446
447
    /**
448
     * @param resource $inStream
449
     * @param resource $outStream
450
     * @param int      $size
451
     *
452
     * @return int
453
     */
454
    private function writeAndCountChecksum($inStream, $outStream, $size)
455
    {
456
        $contextHash = hash_init('crc32b');
457
        $offset = 0;
458
459
        while ($offset < $size) {
460
            $read = min(self::CHUNK_SIZE, $size - $offset);
461
            $buffer = fread($inStream, $read);
462
            fwrite($outStream, $buffer);
463
            hash_update($contextHash, $buffer);
464
            $offset += $read;
465
        }
466
467
        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
    protected function appendCompressionFilter($outStream, ZipEntry $entry)
479
    {
480
        $contextCompress = null;
481
        switch ($entry->getCompressionMethod()) {
482
            case ZipCompressionMethod::DEFLATED:
483
                if (!($contextCompress = stream_filter_append(
484
                    $outStream,
485
                    'zlib.deflate',
486
                    \STREAM_FILTER_WRITE,
487
                    ['level' => $entry->getCompressionLevel()]
488
                ))) {
489
                    throw new \RuntimeException('Could not append filter "zlib.deflate" to out stream');
490
                }
491
                break;
492
493
            case ZipCompressionMethod::BZIP2:
494
                if (!($contextCompress = stream_filter_append(
495
                    $outStream,
496
                    'bzip2.compress',
497
                    \STREAM_FILTER_WRITE,
498
                    ['blocks' => $entry->getCompressionLevel(), 'work' => 0]
499
                ))) {
500
                    throw new \RuntimeException('Could not append filter "bzip2.compress" to out stream');
501
                }
502
                break;
503
504
            case ZipCompressionMethod::STORED:
505
                // file without compression, do nothing
506
                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
        return $contextCompress;
520
    }
521
522
    /**
523
     * @param resource $outStream
524
     * @param ZipEntry $entry
525
     * @param int      $size
526
     *
527
     * @return resource|null
528
     */
529
    protected function appendEncryptionFilter($outStream, ZipEntry $entry, $size)
530
    {
531
        $encContextFilter = null;
532
533
        if ($entry->isEncrypted()) {
534
            if ($entry->getEncryptionMethod() === ZipEncryptionMethod::PKWARE) {
535
                PKEncryptionStreamFilter::register();
536
                $cipherFilterName = PKEncryptionStreamFilter::FILTER_NAME;
537
            } else {
538
                WinZipAesEncryptionStreamFilter::register();
539
                $cipherFilterName = WinZipAesEncryptionStreamFilter::FILTER_NAME;
540
            }
541
            $encContextFilter = stream_filter_append(
542
                $outStream,
543
                $cipherFilterName,
544
                \STREAM_FILTER_WRITE,
545
                [
546
                    'entry' => $entry,
547
                    'size' => $size,
548
                ]
549
            );
550
551
            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
        return $encContextFilter;
557
    }
558
559
    /**
560
     * @param resource $outStream
561
     * @param ZipEntry $entry
562
     */
563
    protected function writeDataDescriptor($outStream, ZipEntry $entry)
564
    {
565
        $crc = $entry->getCrc();
566
567
        /** @var WinZipAesExtraField|null $winZipAesExtra */
568
        $winZipAesExtra = $entry->getLocalExtraField(WinZipAesExtraField::HEADER_ID);
569
570
        if ($winZipAesExtra !== null && $winZipAesExtra->isV2()) {
571
            $crc = 0;
572
        }
573
574
        fwrite(
575
            $outStream,
576
            pack(
577
                'VV',
578
                // data descriptor signature       4 bytes  (0x08074b50)
579
                ZipConstants::DATA_DESCRIPTOR,
580
                // crc-32                          4 bytes
581
                $crc
582
            )
583
        );
584
585
        if (
586
            $entry->isZip64ExtensionsRequired() ||
587
            $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
            $dd = pack(
596
                'VV',
597
                // compressed size                 4 bytes
598
                $entry->getCompressedSize(),
599
                // uncompressed size               4 bytes
600
                $entry->getUncompressedSize()
601
            );
602
        }
603
604
        fwrite($outStream, $dd);
605
    }
606
607
    /**
608
     * @param resource $outStream
609
     *
610
     * @throws ZipException
611
     */
612
    protected function writeCentralDirectoryBlock($outStream)
613
    {
614
        foreach ($this->zipContainer->getEntries() as $outputEntry) {
615
            $this->writeCentralDirectoryHeader($outStream, $outputEntry);
616
        }
617
    }
618
619
    /**
620
     * Writes a Central File Header record.
621
     *
622
     * @param resource $outStream
623
     * @param ZipEntry $entry
624
     *
625
     * @throws ZipException
626
     */
627
    protected function writeCentralDirectoryHeader($outStream, ZipEntry $entry)
628
    {
629
        $compressedSize = $entry->getCompressedSize();
630
        $uncompressedSize = $entry->getUncompressedSize();
631
        $localHeaderOffset = $entry->getLocalHeaderOffset();
632
633
        $entry->getCdExtraFields()->remove(Zip64ExtraField::HEADER_ID);
634
635
        if (
636
            $localHeaderOffset > ZipConstants::ZIP64_MAGIC ||
637
            $compressedSize > ZipConstants::ZIP64_MAGIC ||
638
            $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
        $extra = $this->getExtraFieldsContents($entry, false);
661
        $extraLength = \strlen($extra);
662
663
        $name = $entry->getName();
664
        $comment = $entry->getComment();
665
666
        $dosCharset = $entry->getCharset();
667
668
        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
        $commentLength = \strlen($comment);
677
678
        $compressionMethod = $entry->getCompressionMethod();
679
        $crc = $entry->getCrc();
680
681
        /** @var WinZipAesExtraField|null $winZipAesExtra */
682
        $winZipAesExtra = $entry->getLocalExtraField(WinZipAesExtraField::HEADER_ID);
683
684
        if ($winZipAesExtra !== null) {
685
            if ($winZipAesExtra->isV2()) {
686
                $crc = 0;
687
            }
688
            $compressionMethod = ZipCompressionMethod::WINZIP_AES;
689
        }
690
691
        fwrite(
692
            $outStream,
693
            pack(
694
                'VvvvvVVVVvvvvvVV',
695
                // central file header signature   4 bytes  (0x02014b50)
696
                ZipConstants::CENTRAL_FILE_HEADER,
697
                // version made by                 2 bytes
698
                ($entry->getCreatedOS() << 8) | $entry->getSoftwareVersion(),
699
                // version needed to extract       2 bytes
700
                ($entry->getExtractedOS() << 8) | $entry->getExtractVersion(),
701
                // general purpose bit flag        2 bytes
702
                $entry->getGeneralPurposeBitFlags(),
703
                // compression method              2 bytes
704
                $compressionMethod,
705
                // last mod file datetime          4 bytes
706
                $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
                \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
                0,
721
                // internal file attributes        2 bytes
722
                $entry->getInternalAttributes(),
723
                // external file attributes        4 bytes
724
                $entry->getExternalAttributes(),
725
                // relative offset of local header 4 bytes
726
                $localHeaderOffset
727
            )
728
        );
729
730
        // file name (variable size)
731
        fwrite($outStream, $name);
732
733
        if ($extraLength > 0) {
734
            // extra field (variable size)
735
            fwrite($outStream, $extra);
736
        }
737
738
        if ($commentLength > 0) {
739
            // file comment (variable size)
740
            fwrite($outStream, $comment);
741
        }
742
    }
743
744
    /**
745
     * @param resource $outStream
746
     * @param int      $centralDirectoryOffset
747
     * @param int      $centralDirectorySize
748
     */
749
    protected function writeEndOfCentralDirectoryBlock(
750
        $outStream,
751
        $centralDirectoryOffset,
752
        $centralDirectorySize
753
    ) {
754
        $cdEntriesCount = \count($this->zipContainer);
755
756
        $cdEntriesZip64 = $cdEntriesCount > 0xffff;
757
        $cdSizeZip64 = $centralDirectorySize > ZipConstants::ZIP64_MAGIC;
758
        $cdOffsetZip64 = $centralDirectoryOffset > ZipConstants::ZIP64_MAGIC;
759
760
        $zip64Required = $cdEntriesZip64
761
            || $cdSizeZip64
762
            || $cdOffsetZip64;
763
764
        if ($zip64Required) {
765
            $zip64EndOfCentralDirectoryOffset = ftell($outStream);
766
767
            // find max software version, version needed to extract and most common platform
768
            list($softwareVersion, $versionNeededToExtract) = array_reduce(
769
                $this->zipContainer->getEntries(),
770
                static function (array $carry, ZipEntry $entry) {
771
                    $carry[0] = max($carry[0], $entry->getSoftwareVersion() & 0xFF);
772
                    $carry[1] = max($carry[1], $entry->getExtractVersion() & 0xFF);
773
774
                    return $carry;
775
                },
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