Failed Conditions
Push — master ( 305cd8...275760 )
by Daniel S.
03:34 queued 18s
created

DBC::readStringBlock()   B

Complexity

Conditions 6
Paths 6

Size

Total Lines 30
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 19
CRAP Score 6.0045

Importance

Changes 0
Metric Value
cc 6
eloc 21
nc 6
nop 0
dl 0
loc 30
ccs 19
cts 20
cp 0.95
crap 6.0045
rs 8.9617
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Wowstack\Dbc;
6
7
use Doctrine\Common\Inflector\Inflector;
8
9
/**
10
 * Implements a DBC database file.
11
 */
12
class DBC implements \IteratorAggregate
13
{
14
    /**
15
     * DBC magic number.
16
     */
17
    const SIGNATURE = 'WDBC';
18
19
    /**
20
     * Size of the DBC header.
21
     */
22
    const HEADER_SIZE = 20;
23
24
    /**
25
     * pack format to read a DBC header.
26
     */
27
    const HEADER_PACK_FORMAT = 'V4';
28
29
    /**
30
     * Number of rows contained in the file.
31
     *
32
     * @var int
33
     */
34
    protected $recordCount = 0;
35
36
    /**
37
     * Number of columns contained in the file.
38
     *
39
     * @var int
40
     */
41
    protected $fieldCount = 0;
42
43
    /**
44
     * Size of each row in bytes.
45
     *
46
     * @var int
47
     */
48
    protected $recordSize = 0;
49
50
    /**
51
     * Size of the string block contained in the file.
52
     *
53
     * @var int
54
     */
55
    protected $stringBlockSize = 0;
56
57
    /**
58
     * @var int
59
     */
60
    protected $fileSize = 0;
61
62
    /**
63
     * @var resource|bool
64
     */
65
    protected $fileHandle = null;
66
67
    /**
68
     * @var string
69
     */
70
    protected $filePath = '';
71
72
    /**
73
     * @var Mapping
74
     */
75
    protected $mapping = null;
76
77
    /**
78
     * Offset position to read from for row data.
79
     *
80
     * @var int
81
     */
82
    protected $dataOffset = 0;
83
84
    /**
85
     * Offset position to read from for string data.
86
     *
87
     * @var int
88
     */
89
    protected $stringBlockOffset = 0;
90
91
    /**
92
     * An array of strings with the offset from string block start as index.
93
     *
94
     * @var string[]
95
     */
96
    protected $stringBlock = null;
97
98
    /**
99
     * List of errors occuring while reading data from the DBC file.
100
     *
101
     * @var array
102
     */
103
    protected $errors = [];
104
105
    /**
106
     * Creates a DBC reader.
107
     *
108
     * @param string  $path
109
     * @param Mapping $map
110
     *
111
     * @throws DBCException
112
     */
113 22
    public function __construct(string $path, Mapping $map = null)
114
    {
115 22
        $this->filePath = $path;
116
117 22
        if (!is_file($path) && !is_readable($path)) {
118 2
            throw new DBCException('DBC file not found.');
119
        }
120
121 20
        $this->fileHandle = fopen($this->filePath, 'r');
122 20
        if (false === $this->fileHandle) {
123
            throw new DBCException('DBC file is not readable.');
124
        }
125
126 20
        $this->fileSize = filesize($this->filePath);
127
128 20
        if ($this->fileSize < self::HEADER_SIZE) {
129 3
            throw new DBCException('DBC file is too small.');
130
        }
131
132 17
        $signature = fread($this->fileHandle, strlen(self::SIGNATURE));
133
134 17
        if (self::SIGNATURE !== $signature) {
135
            throw new DBCException('DBC file has invalid signature.');
136
        }
137
138 17
        list(, $this->recordCount, $this->fieldCount, $this->recordSize, $this->stringBlockSize) = unpack(self::HEADER_PACK_FORMAT, fread($this->fileHandle, self::HEADER_SIZE - strlen(self::SIGNATURE)));
0 ignored issues
show
Coding Style introduced by
This line exceeds maximum limit of 120 characters; contains 203 characters

Overly long lines are hard to read on any screen. Most code styles therefor impose a maximum limit on the number of characters in a line.

Loading history...
139
140 17
        $this->dataOffset = self::HEADER_SIZE;
141 17
        $this->stringBlockOffset = self::HEADER_SIZE + ($this->recordCount * $this->recordSize);
142
143 17
        if ($this->fileSize < ($this->stringBlockOffset + $this->stringBlockSize)) {
144
            throw new DBCException('DBC file is too small.');
145
        }
146
147 17
        $this->readStringBlock();
148 17
        $this->attachMapping($map);
149 17
    }
150
151
    /**
152
     * Attaches and verifies a map against the opened DBC file.
153
     *
154
     * @param Mapping $map
155
     *
156
     * @return DBC
157
     *
158
     * @throws DBCException
159
     */
160 17
    public function attachMapping(Mapping $map = null)
161
    {
162 17
        $this->mapping = $map;
163
164 17
        if (null !== $this->mapping) {
165 11
            $delta = $this->mapping->getFieldCount() - $this->getFieldCount();
166 11
            if (0 !== $delta) {
167
                throw new DBCException(
168
                    sprintf('Mapping holds %u fields but DBC holds %u fields.', $this->mapping->getFieldCount(), $this->getFieldCount())
0 ignored issues
show
Coding Style introduced by
This line exceeds maximum limit of 120 characters; contains 136 characters

Overly long lines are hard to read on any screen. Most code styles therefor impose a maximum limit on the number of characters in a line.

Loading history...
169
                );
170
            }
171
172 11
            if ($this->mapping->hasStrings() !== $this->hasStrings()) {
173
                throw new DBCException(
174
                    sprintf('No strings attached! Mapping says %s, DBC says %s.', $this->mapping->hasStrings(), $this->hasStrings())
0 ignored issues
show
Coding Style introduced by
This line exceeds maximum limit of 120 characters; contains 132 characters

Overly long lines are hard to read on any screen. Most code styles therefor impose a maximum limit on the number of characters in a line.

Loading history...
175
                );
176
            }
177
        }
178
179 17
        return $this;
180
    }
181
182
    /**
183
     * Reads in all strings contained within the string block table.
184
     */
185 17
    public function readStringBlock()
186
    {
187 17
        if (false === $this->fileHandle) {
188
            throw new DBCException('DBC file is not readable.');
189
        }
190
191 17
        if ($this->stringBlockSize > 1) {
192 12
            fseek($this->fileHandle, $this->stringBlockOffset);
0 ignored issues
show
Bug introduced by
It seems like $this->fileHandle can also be of type true; however, parameter $handle of fseek() 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

192
            fseek(/** @scrutinizer ignore-type */ $this->fileHandle, $this->stringBlockOffset);
Loading history...
193 12
            $bytesToRead = $this->stringBlockSize;
194 12
            $currentOffset = $this->stringBlockOffset;
195
196 12
            $currentString = null;
197 12
            $bytesRead = 0;
198 12
            while ($bytesToRead > 0) {
199 12
                $currentByte = fread($this->fileHandle, 1);
0 ignored issues
show
Bug introduced by
It seems like $this->fileHandle can also be of type true; however, parameter $handle of fread() 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

199
                $currentByte = fread(/** @scrutinizer ignore-type */ $this->fileHandle, 1);
Loading history...
200 12
                ++$bytesRead;
201 12
                if (chr(0) !== $currentByte) {
202 12
                    $currentString = $currentString.$currentByte;
0 ignored issues
show
Coding Style introduced by
Concat operator must be surrounded by a single space
Loading history...
203
                } else {
204 12
                    if (!empty($currentString)) {
205 12
                        $this->stringBlock[$bytesRead - strlen($currentString)] = $currentString;
206
                    }
207 12
                    $currentString = null;
208
                }
209
210 12
                --$bytesToRead;
211 12
                ++$currentOffset;
212
            }
213
        } else {
214 5
            $this->stringBlock = [];
215
        }
216 17
    }
217
218
    /**
219
     * Returns the filename of the Mapping.
220
     *
221
     * @return string
222
     */
223 3
    public function getName(): string
224
    {
225 3
        return Inflector::singularize(pathinfo($this->getFilePath())['filename']);
226
    }
227
228
    /**
229
     * Returns the canonical path to the file.
230
     *
231
     * @return string
232
     */
233 6
    public function getFilePath(): string
234
    {
235 6
        return realpath($this->filePath);
236
    }
237
238
    /**
239
     * Returns the number of rows in the file.
240
     *
241
     * @return int
242
     */
243 5
    public function getRecordCount(): int
244
    {
245 5
        return $this->recordCount;
246
    }
247
248
    /**
249
     * Returns the number of bytes per row.
250
     *
251
     * @return int
252
     */
253 8
    public function getRecordSize(): int
254
    {
255 8
        return $this->recordSize;
256
    }
257
258
    /**
259
     * Returns the actual amount of colums in the file.
260
     *
261
     * @return int
262
     */
263 11
    public function getFieldCount(): int
264
    {
265 11
        return $this->fieldCount;
266
    }
267
268
    /**
269
     * @return int
270
     */
271 3
    public function getStringBlockSize(): int
272
    {
273 3
        return $this->stringBlockSize;
274
    }
275
276
    /**
277
     * Returns true if the file has a string block.
278
     *
279
     * @return bool
280
     */
281 11
    public function hasStrings(): bool
282
    {
283 11
        return count($this->stringBlock) > 0;
284
    }
285
286
    /**
287
     * Returns all strings found within the file.
288
     *
289
     * @return array
290
     */
291 3
    public function getStringBlock(): array
292
    {
293 3
        return $this->stringBlock;
294
    }
295
296
    /**
297
     * Returns the string from the given offset.
298
     *
299
     * @param int $offset offset in bytes
300
     *
301
     * @return string
302
     *
303
     * @throws DBCException
304
     */
305 2
    public function getString(int $offset): string
306
    {
307 2
        if (array_key_exists($offset + 1, $this->stringBlock)) {
308 2
            return $this->stringBlock[$offset + 1];
309
        }
310
311
        throw new DBCException(sprintf('DBC String Entry not found at index %u', ($offset + 1)));
312
    }
313
314
    /**
315
     * Returns the mapping attach to the DBC.
316
     *
317
     * @return Mapping
318
     */
319 9
    public function getMap()
320
    {
321 9
        return $this->mapping;
322
    }
323
324
    /**
325
     * Returns the handle to the associated file.
326
     *
327
     * @return resource|bool
328
     */
329 5
    public function getFileHandle()
330
    {
331 5
        return $this->fileHandle;
332
    }
333
334
    /**
335
     * Provides an iterator to iterate over the DBC records.
336
     *
337
     * @return DBCIterator
338
     */
339 2
    public function getIterator(): DBCIterator
340
    {
341 2
        return new DBCIterator($this);
342
    }
343
344
    /**
345
     * Returns information if a given record index exists.
346
     *
347
     * @param int $position
348
     *
349
     * @return bool
350
     */
351 6
    public function hasRecord(int $position): bool
352
    {
353 6
        return $position >= 0 && $position < $this->recordCount;
354
    }
355
356
    /**
357
     * Returns the record using the given index.
358
     *
359
     * @param int $position
360
     *
361
     * @return DBCRecord
362
     *
363
     * @throws DBCException
364
     */
365 6
    public function getRecord(int $position): DBCRecord
366
    {
367 6
        if ($this->hasRecord($position)) {
368 5
            return new DBCRecord($this, $position);
369
        }
370
371 1
        throw new DBCException('DBC Record not found.');
372
    }
373
374
    /**
375
     * Appends an error to the list.
376
     *
377
     * @param string $type
378
     * @param int    $position
379
     * @param string $field
380
     * @param string $hint
381
     */
382
    public function addError(string $type, int $position, string $field, string $hint = '')
383
    {
384
        $this->errors[] = [
385
            'type' => $type,
386
            'field' => $field,
387
            'record' => $position,
388
            'hint' => $hint,
389
        ];
390
    }
391
392
    /**
393
     * @return array
394
     */
395
    public function getErrors(): array
396
    {
397
        return $this->errors;
398
    }
399
}
400