ArchiveReader::read()   C
last analyzed

Complexity

Conditions 7
Paths 11

Size

Total Lines 48
Code Lines 25

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 56

Importance

Changes 0
Metric Value
cc 7
eloc 25
nc 11
nop 2
dl 0
loc 48
ccs 0
cts 34
cp 0
crap 56
rs 6.7272
c 0
b 0
f 0
1
<?php
2
3
/*
4
 * (c) 2018 Dennis Birkholz <[email protected]>
5
 *
6
 * $Id$
7
 * Author:    $Format:%an <%ae>, %ai$
8
 * Committer: $Format:%cn <%ce>, %ci$
9
 */
10
11
namespace morgue\zip;
12
13
use morgue\archive\Archive;
14
use morgue\archive\ArchiveReaderInterface;
15
use const iqb\stream\SUBSTREAM_SCHEME;
16
17
/**
18
 * @author Dennis Birkholz <[email protected]>
19
 */
20
final class ArchiveReader implements ArchiveReaderInterface
21
{
22
    public function read($stream, int $offset = 0): Archive
23
    {
24
        $meta = \stream_get_meta_data($stream);
25
        if (!$meta['seekable']) {
26
            throw new \InvalidArgumentException('Zip archive can only be read from a seekable stream.');
27
        }
28
29
        $endOfCentralDirectory = $this->findEndOfCentralDirectory($stream);
30
31
        // Read central directory binary representation
32
        if (\fseek($stream, $offset + $endOfCentralDirectory->getOffsetOfStartOfCentralDirectoryWithRespectToTheStartingDiskNumber()) === -1) {
33
            throw new \RuntimeException("Unable to read Central Directory");
34
        }
35
        $centralDirectoryData = \fread($stream, $endOfCentralDirectory->getSizeOfTheCentralDirectory());
36
37
        $archive = new Archive();
38
        $archive = $archive->withComment($endOfCentralDirectory->getZipFileComment());
39
40
        // Read entries from central directory
41
        for ($position = 0, $i=0; $i<$endOfCentralDirectory->getTotalNumberOfEntriesInTheCentralDirectory(); $i++) {
42
            // Parse central directory entry incl. variable length fields
43
            $centralDirectoryEntry = CentralDirectoryHeader::parse($centralDirectoryData, $position);
44
            $position += CentralDirectoryHeader::MIN_LENGTH;
45
            if ($centralDirectoryEntry->getVariableLength() > 0) {
46
                $position += $centralDirectoryEntry->parseAdditionalData($centralDirectoryData, $position);
47
            }
48
49
            // Seek to local file header, parse it incl. variable length fields
50
            // File handle position is now at the start of the file content
51
            \fseek($stream, $offset + $centralDirectoryEntry->getRelativeOffsetOfLocalHeader());
52
            $localHeader = LocalFileHeader::parse(\fread($stream, LocalFileHeader::MIN_LENGTH));
53
            if ($localHeader->getVariableLength() > 0) {
54
                $localHeader->parseAdditionalData(\fread($stream, $localHeader->getVariableLength()));
55
            }
56
57
            // Create archive entry and attach content stream for non-directories
58
            $archiveEntry = $centralDirectoryEntry->toArchiveEntry();
59
            if (!$centralDirectoryEntry->isDirectory()) {
60
                $archiveEntry = $archiveEntry->withSourceStream(
61
                    \fopen(SUBSTREAM_SCHEME . '://' . \ftell($stream) . ':' . $centralDirectoryEntry->getCompressedSize() . '/' . (int)$stream, 'r')
62
                );
63
            }
64
65
            $archive = $archive->addEntry($archiveEntry);
66
        }
67
68
        return $archive;
69
    }
70
71
    /**
72
     * @param resource $stream
73
     * @return EndOfCentralDirectory
74
     */
75
    private function findEndOfCentralDirectory($stream)
76
    {
77
        $signature = \pack('N', EndOfCentralDirectory::SIGNATURE);
78
79
        for ($offset = EndOfCentralDirectory::MIN_LENGTH; $offset <= EndOfCentralDirectory::MAX_LENGTH; $offset++) {
80
            if (\fseek($stream, -$offset, \SEEK_END) === -1) {
81
                throw new \RuntimeException("Can not find EndOfDirectoryDirectory record");
82
            }
83
84
            $chunk = \fread($stream, EndOfCentralDirectory::MIN_LENGTH);
85
            if (\substr($chunk, 0, \strlen($signature)) !== $signature) {
86
                continue;
87
            }
88
89
            $endOfCentralDirectory = EndOfCentralDirectory::parse($chunk);
90
            if ($endOfCentralDirectory->getVariableLength() > 0) {
91
                $additionalData = \fread($stream, $endOfCentralDirectory->getVariableLength());
92
                $endOfCentralDirectory->parseAdditionalData($additionalData);
93
            }
94
95
            return $endOfCentralDirectory;
96
        }
97
98
        throw new \RuntimeException("Unable to read Central Directory");
99
    }
100
}
101