Completed
Push — master ( 5cbbf9...fe28cf )
by Michele
05:41 queued 01:59
created

CHM::__construct()   B

Complexity

Conditions 5
Paths 5

Size

Total Lines 30
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 21
CRAP Score 5.0164

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 30
ccs 21
cts 23
cp 0.913
rs 8.439
cc 5
eloc 21
nc 5
nop 1
crap 5.0164
1
<?php
2
3
namespace CHMLib;
4
5
use Exception;
6
use CHMLib\Reader\Reader;
7
use CHMLib\Reader\StringReader;
8
use CHMLib\Exception\UnexpectedHeaderException;
9
use CHMLib\Reader\FileReader;
10
11
/**
12
 * Handle the contents of a CHM file.
13
 */
14
class CHM
15
{
16
    /**
17
     * The reader that provides the data.
18
     *
19
     * @var Reader
20
     */
21
    protected $reader;
22
23
    /**
24
     * The CHM initial header.
25
     *
26
     * @var Header\ITSF
27
     */
28
    protected $itsf;
29
30
    /**
31
     * The directory listing header.
32
     *
33
     * @var Header\ITSP
34
     */
35
    protected $itsp;
36
37
    /**
38
     * The entries found in this CHM.
39
     *
40
     * @var Entry[]
41
     */
42
    protected $entries;
43
44
    /**
45
     * The data sections.
46
     *
47
     * @var Section\Section[]
48
     */
49
    protected $sections;
50
51
    /**
52
     * Initializes the instance.
53
     *
54
     * @param Reader $reader The reader that provides the data.
55
     *
56
     * @throws Exception Throws an Exception in case of errors.
57
     */
58 1
    public function __construct(Reader $reader)
59
    {
60 1
        $this->reader = $reader;
61 1
        $reader->setPosition(0);
62 1
        $this->itsf = new Header\ITSF($reader);
63 1
        if ($this->itsf->getSectionOffset() >= 0 && $this->itsf->getSectionLength() >= 16 /* === 24*/) {
64 1
            $reader->setPosition($this->itsf->getSectionOffset());
65 1
            /* Unknown (510) */ $reader->readUInt32();
0 ignored issues
show
Unused Code Comprehensibility introduced by
50% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
66 1
            /* Unknown (0) */ $reader->readUInt32();
0 ignored issues
show
Unused Code Comprehensibility introduced by
50% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
67 1
            $totalLength = $reader->readUInt64();
68 1
            if ($totalLength !== $reader->getLength()) {
69
                throw new Exception("Invalid CHM size: expected length $totalLength, current length {$reader->getLength()}");
70
            }
71 1
        }
72 1
        $reader->setPosition($this->itsf->getDirectoryOffset());
73 1
        $this->itsp = new Header\ITSP($reader);
74
75 1
        $expectedDirectoryLength = $this->itsf->getDirectoryLength();
76 1
        $calculatedDirectoryLength = $this->itsp->getHeaderLength() + $this->itsp->getNumberOfDirectoryChunks() * $this->itsp->getDirectoryChunkSize();
77 1
        if ($expectedDirectoryLength !== $calculatedDirectoryLength) {
78
            throw new Exception("Unexpected directory list size (expected: $expectedDirectoryLength, calculated: $calculatedDirectoryLength)");
79
        }
80
81 1
        $this->sections = array();
82 1
        $this->sections[0] = new Section\UncompressedSection($this);
83
84 1
        $this->entries = $this->retrieveEntryList();
85
86 1
        $this->retrieveSectionList();
87 1
    }
88
89
    /**
90
     * Destruct the instance.
91
     */
92
    public function __destruct()
93
    {
94
        unset($this->reader);
95
    }
96
97
    /**
98
     * Create a new CHM instance reading a file.
99
     *
100
     * @param string $filename
101
     *
102
     * @return static
103
     */
104 1
    public static function fromFile($filename)
105
    {
106 1
        $reader = new FileReader($filename);
107
108 1
        return new static($reader);
109
    }
110
111
    /**
112
     * Get the reader that provides the data.
113
     *
114
     * @return Reader
115
     */
116 465
    public function getReader()
117
    {
118 465
        return $this->reader;
119
    }
120
121
    /**
122
     * Get the CHM initial header.
123
     *
124
     * @return Header\ITSF
125
     */
126 1
    public function getITSF()
127
    {
128 1
        return $this->itsf;
129
    }
130
131
    /**
132
     * Get the directory listing header.
133
     *
134
     * @return Header\ITSP
135
     */
136
    public function getITSP()
137
    {
138
        return $this->itsp;
139
    }
140
141
    /**
142
     * Get an entry given its full path.
143
     *
144
     * @param string $path The full path (case sensitive) of the entry to look for.
145
     *
146
     * @return Entry|null
147
     */
148 466
    public function getEntryByPath($path)
149
    {
150 466
        return isset($this->entries[$path]) ? $this->entries[$path] : null;
151
    }
152
153
    /**
154
     * Get the entries contained in this CHM.
155
     *
156
     * @param int|null $type One or more Entry::TYPE_... values (defaults to Entry::TYPE_FILE | Entry::TYPE_DIRECTORY if null).
157
     */
158 1
    public function getEntries($type = null)
159
    {
160 1
        if ($type === null) {
161
            $type = Entry::TYPE_FILE | Entry::TYPE_DIRECTORY;
162
        }
163 1
        $result = array();
164 1
        foreach ($this->entries as $entry) {
165 1
            if (($entry->getType() & $type) !== 0) {
166 1
                $result[] = $entry;
167 1
            }
168 1
        }
169
170 1
        return $result;
171
    }
172
173
    /**
174
     * Return a section given its index.
175
     *
176
     * @param int $i
177
     *
178
     * @return Section\Section|null
179
     */
180 466
    public function getSectionByIndex($i)
181
    {
182 466
        return isset($this->sections[$i]) ? $this->sections[$i] : null;
183
    }
184
185
    /**
186
     * Retrieve the list of the entries contained in this CHM.
187
     *
188
     * @return Entry[]
189
     *
190
     * @throws Exception Throws an Exception in case of errors.
191
     */
192 1
    protected function retrieveEntryList()
193
    {
194 1
        $result = array();
195 1
        $chunkOffset = $this->itsf->getDirectoryOffset() + $this->itsp->getHeaderLength();
196 1
        $chunkSize = $this->itsp->getDirectoryChunkSize();
197 1
        for ($i = $this->itsp->getFirstPMGLChunkNumber(), $l = $this->itsp->getLastPMGLChunkNumber(); $i <= $l; ++$i) {
198 1
            $offset = $chunkOffset + $i * $chunkSize;
199 1
            $this->reader->setPosition($offset);
200
            try {
201 1
                $pmgl = new Header\PMGL($this->reader);
202 1
            } catch (UnexpectedHeaderException $x) {
203
                if ($x->getFoundHeader() !== 'PMGI') {
204
                    throw $x;
205
                }
206
                $this->reader->setPosition($offset);
207
                $pmgi = new Header\PMGI($this->reader);
0 ignored issues
show
Unused Code introduced by
$pmgi is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
208
                $pmgl = null;
209
            }
210 1
            if ($pmgl !== null) {
211 1
                $end = $offset + $chunkSize - $pmgl->getFreeSpace();
212 1
                $cur = $this->reader->getPosition();
213 1
                while ($cur < $end) {
214 1
                    $this->reader->setPosition($cur);
215 1
                    $entry = new Entry($this);
216 1
                    $result[$entry->getPath()] = $entry;
217 1
                    $cur = $this->reader->getPosition();
218 1
                }
219 1
            }
220 1
        }
221
222 1
        return $result;
223
    }
224
225
    /**
226
     * Retrieve the list of the data sections contained in this CHM.
227
     *
228
     * @throws Exception Throws an Exception in case of errors.
229
     */
230 1
    protected function retrieveSectionList()
231
    {
232 1
        $nameList = $this->getEntryByPath('::DataSpace/NameList');
233
234 1
        if ($nameList === null) {
235
            throw new Exception("Missing required entry: '::DataSpace/NameList'");
236
        }
237 1
        if ($nameList->getContentSectionIndex() !== 0) {
238
            throw new Exception("The content of the entry '{$nameList->getPath()}' should be in section 0, but it's in section {$nameList->getContentSection()}");
0 ignored issues
show
Bug introduced by
The method getContentSection() does not exist on CHMLib\Entry. Did you maybe mean getContentSectionIndex()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
239
        }
240
241 1
        $nameListReader = new StringReader($nameList->getContents());
242 1
        /* Length */ $nameListReader->readUInt16();
243 1
        $numSections = $nameListReader->readUInt16();
244 1
        if ($numSections === 0) {
245
            throw new Exception('No content section defined.');
246
        }
247 1
        for ($i = 0; $i < $numSections; ++$i) {
248 1
            $nameLength = $nameListReader->readUInt16();
249 1
            $utf16name = $nameListReader->readString($nameLength * 2);
250 1
            $nameListReader->readUInt16();
251 1
            $name = iconv('UTF-16LE', 'UTF-8', $utf16name);
252
            switch ($name) {
253 1
                case 'Uncompressed':
254 1
                    break;
255 1
                case 'MSCompressed':
256 1
                    if ($i === 0) {
257
                        throw new Exception('First data section should be Uncompressed');
258
                    } else {
259 1
                        $this->sections[$i] = new Section\MSCompressedSection($this);
260
                    }
261 1
                    break;
262
                default:
263
                    throw new Exception("Unknown data section: $name");
264
            }
265 1
        }
266 1
    }
267
}
268