Completed
Pull Request — master (#6)
by Michele
03:20
created

CHM::__construct()   A

Complexity

Conditions 5
Paths 5

Size

Total Lines 33

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 23
CRAP Score 5.0128

Importance

Changes 0
Metric Value
dl 0
loc 33
ccs 23
cts 25
cp 0.92
rs 9.0808
c 0
b 0
f 0
cc 5
nc 5
nop 1
crap 5.0128
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 4
        }
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 (case sensitive) of the entry to look for.
162
     *
163
     * @return \CHMLib\Entry|null
164
     */
165 491
    public function getEntryByPath($path)
166
    {
167 491
        return isset($this->entries[$path]) ? $this->entries[$path] : null;
168
    }
169
170
    /**
171
     * Get the entries contained in this CHM.
172
     *
173
     * @param int|null $type One or more Entry::TYPE_... values (defaults to Entry::TYPE_FILE | Entry::TYPE_DIRECTORY if null).
174
     */
175 3
    public function getEntries($type = null)
176
    {
177 3
        if ($type === null) {
178
            $type = Entry::TYPE_FILE | Entry::TYPE_DIRECTORY;
179
        }
180 3
        $result = array();
181 3
        foreach ($this->entries as $entry) {
182 3
            if (($entry->getType() & $type) !== 0) {
183 3
                $result[] = $entry;
184 3
            }
185 3
        }
186
187 3
        return $result;
188
    }
189
190
    /**
191
     * Return a section given its index.
192
     *
193
     * @param int $i
194
     *
195
     * @return \CHMLib\Section\Section|null
196
     */
197 491
    public function getSectionByIndex($i)
198
    {
199 491
        return isset($this->sections[$i]) ? $this->sections[$i] : null;
200
    }
201
202
    /**
203
     * Retrieve the list of the entries contained in this CHM.
204
     *
205
     * @throws \Exception Throws an Exception in case of errors.
206
     *
207
     * @return \CHMLib\Entry[]
208
     */
209 4
    protected function retrieveEntryList()
210
    {
211 4
        $result = array();
212 4
        $chunkOffset = $this->itsf->getDirectoryOffset() + $this->itsp->getHeaderLength();
213 4
        $chunkSize = $this->itsp->getDirectoryChunkSize();
214 4
        for ($i = $this->itsp->getFirstPMGLChunkNumber(), $l = $this->itsp->getLastPMGLChunkNumber(); $i <= $l; ++$i) {
215 4
            $offset = $chunkOffset + $i * $chunkSize;
216 4
            $this->reader->setPosition($offset);
217
            try {
218 4
                $pmgl = new Header\PMGL($this->reader);
219 4
            } catch (UnexpectedHeaderException $x) {
220
                if ($x->getFoundHeader() !== 'PMGI') {
221
                    throw $x;
222
                }
223
                $this->reader->setPosition($offset);
224
                new Header\PMGI($this->reader);
225
                $pmgl = null;
226
            }
227 4
            if ($pmgl !== null) {
228 4
                $end = $offset + $chunkSize - $pmgl->getFreeSpace();
229 4
                $cur = $this->reader->getPosition();
230 4
                while ($cur < $end) {
231 4
                    $this->reader->setPosition($cur);
232 4
                    $entry = new Entry($this);
233 4
                    $result[$entry->getPath()] = $entry;
234 4
                    $cur = $this->reader->getPosition();
235 4
                }
236 4
            }
237 4
        }
238
239 4
        return $result;
240
    }
241
242
    /**
243
     * Retrieve the list of the data sections contained in this CHM.
244
     *
245
     * @throws \Exception Throws an Exception in case of errors.
246
     */
247 4
    protected function retrieveSectionList()
248
    {
249 4
        $nameList = $this->getEntryByPath('::DataSpace/NameList');
250
251 4
        if ($nameList === null) {
252
            throw new Exception("Missing required entry: '::DataSpace/NameList'");
253
        }
254 4
        if ($nameList->getContentSectionIndex() !== 0) {
255
            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...
256
        }
257
258 4
        $nameListReader = new StringReader($nameList->getContents());
259 4
        /* Length */ $nameListReader->readUInt16();
260 4
        $numSections = $nameListReader->readUInt16();
261 4
        if ($numSections === 0) {
262
            throw new Exception('No content section defined.');
263
        }
264 4
        for ($i = 0; $i < $numSections; ++$i) {
265 4
            $nameLength = $nameListReader->readUInt16();
266 4
            $utf16name = $nameListReader->readString($nameLength * 2);
267 4
            $nameListReader->readUInt16();
268 4
            $name = iconv('UTF-16LE', 'UTF-8', $utf16name);
269
            switch ($name) {
270 4
                case 'Uncompressed':
271 4
                    break;
272 4
                case 'MSCompressed':
273 4
                    if ($i === 0) {
274
                        throw new Exception('First data section should be Uncompressed');
275
                    } else {
276 4
                        $this->sections[$i] = new Section\MSCompressedSection($this);
277
                    }
278 4
                    break;
279
                default:
280
                    throw new Exception("Unknown data section: $name");
281
            }
282 4
        }
283 4
    }
284
285
    /**
286
     * Get the TOC of this CHM file (if available).
287
     *
288
     * @return \CHMLib\TOCIndex\Tree|null
289
     */
290 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...
291
    {
292 1
        if ($this->toc === null) {
293 1
            $r = false;
294 1
            foreach ($this->entries as $entry) {
295 1
                if ($entry->isFile() && strcasecmp(substr($entry->getPath(), -4), '.hhc') === 0) {
296 1
                    $r = TOCIndex\Tree::fromString($this, $entry->getContents());
297 1
                    break;
298
                }
299 1
            }
300 1
            $this->toc = $r;
301 1
        }
302
303 1
        return ($this->toc === false) ? null : $this->toc;
304
    }
305
306
    /**
307
     * Get the index of this CHM file (if available).
308
     *
309
     * @return \CHMLib\TOCIndex\Tree|null
310
     */
311 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...
312
    {
313
        if ($this->index === null) {
314
            $r = false;
315
            foreach ($this->entries as $entry) {
316
                if ($entry->isFile() && strcasecmp(substr($entry->getPath(), -4), '.hhk') === 0) {
317
                    $r = TOCIndex\Tree::fromString($this, $entry->getContents());
318
                    break;
319
                }
320
            }
321
            $this->index = $r;
322
        }
323
324
        return ($this->index === false) ? null : $this->index;
325
    }
326
}
327