CHM   B
last analyzed

Complexity

Total Complexity 46

Size/Duplication

Total Lines 322
Duplicated Lines 9.32 %

Coupling/Cohesion

Components 2
Dependencies 12

Test Coverage

Coverage 72.72%

Importance

Changes 0
Metric Value
wmc 46
lcom 2
cbo 12
dl 30
loc 322
ccs 88
cts 121
cp 0.7272
rs 8.72
c 0
b 0
f 0

13 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 33 5
A __destruct() 0 4 1
A fromFile() 0 6 1
A getReader() 0 4 1
A getITSF() 0 4 1
A getITSP() 0 4 1
A getEntryByPath() 0 12 4
A getEntries() 0 14 4
A getSectionByIndex() 0 4 2
B retrieveEntryList() 0 32 6
B retrieveSectionList() 0 37 8
A getTOC() 15 15 6
A getIndex() 15 15 6

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like CHM often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use CHM, and based on these observations, apply Extract Interface, too.

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 \CHMLib\Reader\Reader
20
     */
21
    protected $reader;
22
23
    /**
24
     * The CHM initial header.
25
     *
26
     * @var \CHMLib\Header\ITSF
27
     */
28
    protected $itsf;
29
30
    /**
31
     * The directory listing header.
32
     *
33
     * @var \CHMLib\Header\ITSP
34
     */
35
    protected $itsp;
36
37
    /**
38
     * The entries found in this CHM.
39
     *
40
     * @var \CHMLib\Entry[]
41
     */
42
    protected $entries;
43
44
    /**
45
     * The data sections.
46
     *
47
     * @var \CHMLib\Section\Section[]
48
     */
49
    protected $sections;
50
51
    /**
52
     * The TOC.
53
     *
54
     * @var \CHMLib\TOCIndex\Tree|null|false
55
     */
56
    protected $toc;
57
58
    /**
59
     * The index.
60
     *
61
     * @var \CHMLib\TOCIndex\Tree|null|false
62
     */
63
    protected $index;
64
65
    /**
66
     * Initializes the instance.
67
     *
68
     * @param \CHMLib\Reader\Reader $reader The reader that provides the data.
69
     *
70
     * @throws \Exception Throws an Exception in case of errors.
71
     */
72 4
    public function __construct(Reader $reader)
73
    {
74 4
        $this->reader = $reader;
75 4
        $reader->setPosition(0);
76 4
        $this->itsf = new Header\ITSF($reader);
77 4
        if ($this->itsf->getSectionOffset() >= 0 && $this->itsf->getSectionLength() >= 16 /* === 24*/) {
78 4
            $reader->setPosition($this->itsf->getSectionOffset());
79 4
            /* Unknown (510) */ $reader->readUInt32();
80 4
            /* Unknown (0) */ $reader->readUInt32();
81 4
            $totalLength = $reader->readUInt64();
82 4
            if ($totalLength !== $reader->getLength()) {
83
                throw new Exception("Invalid CHM size: expected length $totalLength, current length {$reader->getLength()}");
84
            }
85
        }
86 4
        $reader->setPosition($this->itsf->getDirectoryOffset());
87 4
        $this->itsp = new Header\ITSP($reader);
88
89 4
        $expectedDirectoryLength = $this->itsf->getDirectoryLength();
90 4
        $calculatedDirectoryLength = $this->itsp->getHeaderLength() + $this->itsp->getNumberOfDirectoryChunks() * $this->itsp->getDirectoryChunkSize();
91 4
        if ($expectedDirectoryLength !== $calculatedDirectoryLength) {
92
            throw new Exception("Unexpected directory list size (expected: $expectedDirectoryLength, calculated: $calculatedDirectoryLength)");
93
        }
94
95 4
        $this->sections = array();
96 4
        $this->sections[0] = new Section\UncompressedSection($this);
97
98 4
        $this->entries = $this->retrieveEntryList();
99
100 4
        $this->retrieveSectionList();
101
102 4
        $this->toc = null;
103 4
        $this->index = null;
104 4
    }
105
106
    /**
107
     * Destruct the instance.
108
     */
109
    public function __destruct()
110
    {
111
        unset($this->reader);
112
    }
113
114
    /**
115
     * Create a new CHM instance reading a file.
116
     *
117
     * @param string $filename
118
     *
119
     * @return static
120
     */
121 4
    public static function fromFile($filename)
122
    {
123 4
        $reader = new FileReader($filename);
124
125 4
        return new static($reader);
126
    }
127
128
    /**
129
     * Get the reader that provides the data.
130
     *
131
     * @return \CHMLib\Reader\Reader
132
     */
133 486
    public function getReader()
134
    {
135 486
        return $this->reader;
136
    }
137
138
    /**
139
     * Get the CHM initial header.
140
     *
141
     * @return \CHMLib\Header\ITSF
142
     */
143 4
    public function getITSF()
144
    {
145 4
        return $this->itsf;
146
    }
147
148
    /**
149
     * Get the directory listing header.
150
     *
151
     * @return \CHMLib\Header\ITSP
152
     */
153
    public function getITSP()
154
    {
155
        return $this->itsp;
156
    }
157
158
    /**
159
     * Get an entry given its full path.
160
     *
161
     * @param string $path The full path of the entry to look for.
162
     * @param bool $caseSensitive Perform a case-sensitive search?
163
     *
164
     * @return \CHMLib\Entry|null
165
     */
166 491
    public function getEntryByPath($path, $caseSensitive = false)
167
    {
168 491
        if (isset($this->entries[$path])) {
169 491
            return $this->entries[$path];
170
        }
171
        if ($caseSensitive) {
172
            return null;
173
        }
174
        $entries = array_change_key_case($this->entries, CASE_LOWER);
175
        $path = strtolower($path);
176
        return isset($entries[$path]) ? $entries[$path] : null;
177
    }
178
179
    /**
180
     * Get the entries contained in this CHM.
181
     *
182
     * @param int|null $type One or more Entry::TYPE_... values (defaults to Entry::TYPE_FILE | Entry::TYPE_DIRECTORY if null).
183
     */
184 3
    public function getEntries($type = null)
185
    {
186 3
        if ($type === null) {
187
            $type = Entry::TYPE_FILE | Entry::TYPE_DIRECTORY;
188
        }
189 3
        $result = array();
190 3
        foreach ($this->entries as $entry) {
191 3
            if (($entry->getType() & $type) !== 0) {
192 3
                $result[] = $entry;
193
            }
194
        }
195
196 3
        return $result;
197
    }
198
199
    /**
200
     * Return a section given its index.
201
     *
202
     * @param int $i
203
     *
204
     * @return \CHMLib\Section\Section|null
205
     */
206 491
    public function getSectionByIndex($i)
207
    {
208 491
        return isset($this->sections[$i]) ? $this->sections[$i] : null;
209
    }
210
211
    /**
212
     * Retrieve the list of the entries contained in this CHM.
213
     *
214
     * @throws \Exception Throws an Exception in case of errors.
215
     *
216
     * @return \CHMLib\Entry[]
217
     */
218 4
    protected function retrieveEntryList()
219
    {
220 4
        $result = array();
221 4
        $chunkOffset = $this->itsf->getDirectoryOffset() + $this->itsp->getHeaderLength();
222 4
        $chunkSize = $this->itsp->getDirectoryChunkSize();
223 4
        for ($i = $this->itsp->getFirstPMGLChunkNumber(), $l = $this->itsp->getLastPMGLChunkNumber(); $i <= $l; ++$i) {
224 4
            $offset = $chunkOffset + $i * $chunkSize;
225 4
            $this->reader->setPosition($offset);
226
            try {
227 4
                $pmgl = new Header\PMGL($this->reader);
228
            } catch (UnexpectedHeaderException $x) {
229
                if ($x->getFoundHeader() !== 'PMGI') {
230
                    throw $x;
231
                }
232
                $this->reader->setPosition($offset);
233
                new Header\PMGI($this->reader);
234
                $pmgl = null;
235
            }
236 4
            if ($pmgl !== null) {
237 4
                $end = $offset + $chunkSize - $pmgl->getFreeSpace();
238 4
                $cur = $this->reader->getPosition();
239 4
                while ($cur < $end) {
240 4
                    $this->reader->setPosition($cur);
241 4
                    $entry = new Entry($this);
242 4
                    $result[$entry->getPath()] = $entry;
243 4
                    $cur = $this->reader->getPosition();
244
                }
245
            }
246
        }
247
248 4
        return $result;
249
    }
250
251
    /**
252
     * Retrieve the list of the data sections contained in this CHM.
253
     *
254
     * @throws \Exception Throws an Exception in case of errors.
255
     */
256 4
    protected function retrieveSectionList()
257
    {
258 4
        $nameList = $this->getEntryByPath('::DataSpace/NameList');
259
260 4
        if ($nameList === null) {
261
            throw new Exception("Missing required entry: '::DataSpace/NameList'");
262
        }
263 4
        if ($nameList->getContentSectionIndex() !== 0) {
264
            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...
265
        }
266
267 4
        $nameListReader = new StringReader($nameList->getContents());
268 4
        /* Length */ $nameListReader->readUInt16();
269 4
        $numSections = $nameListReader->readUInt16();
270 4
        if ($numSections === 0) {
271
            throw new Exception('No content section defined.');
272
        }
273 4
        for ($i = 0; $i < $numSections; ++$i) {
274 4
            $nameLength = $nameListReader->readUInt16();
275 4
            $utf16name = $nameListReader->readString($nameLength * 2);
276 4
            $nameListReader->readUInt16();
277 4
            $name = iconv('UTF-16LE', 'UTF-8', $utf16name);
278 4
            switch ($name) {
279 4
                case 'Uncompressed':
280 4
                    break;
281 4
                case 'MSCompressed':
282 4
                    if ($i === 0) {
283
                        throw new Exception('First data section should be Uncompressed');
284
                    } else {
285 4
                        $this->sections[$i] = new Section\MSCompressedSection($this);
286
                    }
287 4
                    break;
288
                default:
289
                    throw new Exception("Unknown data section: $name");
290
            }
291
        }
292 4
    }
293
294
    /**
295
     * Get the TOC of this CHM file (if available).
296
     *
297
     * @return \CHMLib\TOCIndex\Tree|null
298
     */
299 1 View Code Duplication
    public function getTOC()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
300
    {
301 1
        if ($this->toc === null) {
302 1
            $r = false;
303 1
            foreach ($this->entries as $entry) {
304 1
                if ($entry->isFile() && strcasecmp(substr($entry->getPath(), -4), '.hhc') === 0) {
305 1
                    $r = TOCIndex\Tree::fromString($this, $entry->getContents());
306 1
                    break;
307
                }
308
            }
309 1
            $this->toc = $r;
310
        }
311
312 1
        return ($this->toc === false) ? null : $this->toc;
313
    }
314
315
    /**
316
     * Get the index of this CHM file (if available).
317
     *
318
     * @return \CHMLib\TOCIndex\Tree|null
319
     */
320 View Code Duplication
    public function getIndex()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
321
    {
322
        if ($this->index === null) {
323
            $r = false;
324
            foreach ($this->entries as $entry) {
325
                if ($entry->isFile() && strcasecmp(substr($entry->getPath(), -4), '.hhk') === 0) {
326
                    $r = TOCIndex\Tree::fromString($this, $entry->getContents());
327
                    break;
328
                }
329
            }
330
            $this->index = $r;
331
        }
332
333
        return ($this->index === false) ? null : $this->index;
334
    }
335
}
336