Reader   A
last analyzed

Complexity

Total Complexity 28

Size/Duplication

Total Lines 283
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 0

Importance

Changes 0
Metric Value
wmc 28
lcom 1
cbo 0
dl 0
loc 283
rs 10
c 0
b 0
f 0

13 Methods

Rating   Name   Duplication   Size   Complexity  
A load() 0 15 1
A readStringTables() 0 14 1
A readTranslations() 0 13 2
A processRecord() 0 14 4
A readMsgId() 0 9 2
A readTranslation() 0 9 2
A readStringFromTable() 0 13 2
A calcSizeKey() 0 4 1
A openFile() 0 23 3
A determineByteOrder() 0 18 3
A verifyMajorRevision() 0 14 3
A readInteger() 0 10 2
A readIntegerList() 0 8 2
1
<?php
2
3
namespace MoReader;
4
5
/**
6
 * Gettext loader
7
 */
8
class Reader
9
{
10
    /**
11
     * Current file pointer.
12
     *
13
     * @var resource
14
     */
15
    protected $file;
16
17
    /**
18
     * @var string
19
     */
20
    protected $filename;
21
22
    /**
23
     * Whether the current file is little endian.
24
     *
25
     * @var bool
26
     */
27
    protected $littleEndian;
28
29
    /**
30
     * @var int
31
     */
32
    protected $stringsCount;
33
34
    /**
35
     * @var array
36
     */
37
    protected $msgIdTable;
38
39
    /**
40
     * @var array
41
     */
42
    protected $msgStrTable;
43
44
    /**
45
     *
46
     * @param string $filename
47
     * @return array
48
     * @throws \Exception
49
     */
50
    public function load($filename)
51
    {
52
        $this->openFile($filename);
53
54
        $this->determineByteOrder();
55
        $this->verifyMajorRevision();
56
57
        $this->readStringTables();
58
59
        $data = $this->readTranslations();
60
61
        fclose($this->file);
62
63
        return $data;
64
    }
65
66
    /**
67
     *
68
     */
69
    protected function readStringTables()
70
    {
71
        $this->stringsCount = $this->readInteger();
72
        $msgIdTableOffset = $this->readInteger();
73
        $msgStrTableOffset = $this->readInteger();
74
75
        // Usually there follow size and offset of the hash table, but we have
76
        // no need for it, so we skip them.
77
        fseek($this->file, $msgIdTableOffset);
78
        $this->msgIdTable = $this->readIntegerList(2 * $this->stringsCount);
79
80
        fseek($this->file, $msgStrTableOffset);
81
        $this->msgStrTable = $this->readIntegerList(2 * $this->stringsCount);
82
    }
83
84
    /**
85
     * @return array
86
     */
87
    protected function readTranslations()
88
    {
89
        $data = array();
90
91
        for ($counter = 0; $counter < $this->stringsCount; $counter++) {
92
            $msgId = $this->readMsgId($counter);
93
            $msgStr = $this->readTranslation($counter);
94
95
            $this->processRecord($data, $msgId, $msgStr);
96
        }
97
98
        return $data;
99
    }
100
101
    /**
102
     * @param array $data
103
     * @param array $msgId
104
     * @param array $msgStr
105
     */
106
    protected function processRecord(&$data, $msgId, $msgStr)
107
    {
108
        if (count($msgId) > 1 && count($msgStr) > 1) {
109
            $data[$msgId[0]] = $msgStr;
110
111
            array_shift($msgId);
112
113
            foreach ($msgId as $string) {
114
                $data[$string] = '';
115
            }
116
        } else {
117
            $data[$msgId[0]] = $msgStr[0];
118
        }
119
    }
120
121
    /**
122
     * Reads specified message id record
123
     *
124
     * @param int $index
125
     *
126
     * @return array
127
     */
128
    protected function readMsgId($index)
129
    {
130
        $msgId = $this->readStringFromTable($index, $this->msgIdTable);
131
        if (false === $msgId) {
132
            $msgId = array('');
133
        }
134
135
        return $msgId;
136
    }
137
138
    /**
139
     * Reads specified translation record
140
     *
141
     * @param int $index
142
     *
143
     * @return array
144
     */
145
    protected function readTranslation($index)
146
    {
147
        $msgStr = $this->readStringFromTable($index, $this->msgStrTable);
148
        if (false === $msgStr) {
149
            $msgStr = array();
150
        }
151
152
        return $msgStr;
153
    }
154
155
    /**
156
     * @param int $index
157
     * @param array $table
158
     *
159
     * @return array|bool
160
     */
161
    protected function readStringFromTable($index, $table)
162
    {
163
        $sizeKey = $this->calcSizeKey($index);
164
        $size = $table[$sizeKey];
165
166
        if ($size > 0) {
167
            $offset = $table[$sizeKey + 1];
168
            fseek($this->file, $offset);
169
            return explode("\0", fread($this->file, $size));
170
        }
171
172
        return false;
173
    }
174
175
    /**
176
     * @param $counter
177
     *
178
     * @return int
179
     */
180
    protected function calcSizeKey($counter)
181
    {
182
        return $counter * 2 + 1;
183
    }
184
185
    /**
186
     * Prepare file for reading
187
     *
188
     * @param $filename
189
     *
190
     * @throws \Exception
191
     */
192
    protected function openFile($filename)
193
    {
194
        $this->filename = $filename;
195
196
        if (!is_file($this->filename)) {
197
            throw new \Exception(
198
                sprintf(
199
                    'File %s does not exist',
200
                    $this->filename
201
                )
202
            );
203
        }
204
205
        $this->file = fopen($this->filename, 'rb');
206
        if (false === $this->file) {
207
            throw new \Exception(
208
                sprintf(
209
                    'Could not open file %s for reading',
210
                    $this->filename
211
                )
212
            );
213
        }
214
    }
215
216
    /**
217
     * Determines byte order
218
     *
219
     * @throws \Exception
220
     */
221
    protected function determineByteOrder()
222
    {
223
        $orderHeader = fread($this->file, 4);
224
225
        if ($orderHeader == "\x95\x04\x12\xde") {
226
            $this->littleEndian = false;
227
        } elseif ($orderHeader == "\xde\x12\x04\x95") {
228
            $this->littleEndian = true;
229
        } else {
230
            fclose($this->file);
231
            throw new \Exception(
232
                sprintf(
233
                    '%s is not a valid gettext file',
234
                    $this->filename
235
                )
236
            );
237
        }
238
    }
239
240
    /**
241
     * Verify major revision (only 0 and 1 supported)
242
     *
243
     * @throws \Exception
244
     */
245
    protected function verifyMajorRevision()
246
    {
247
        $majorRevision = ($this->readInteger() >> 16);
248
249
        if ($majorRevision !== 0 && $majorRevision !== 1) {
250
            fclose($this->file);
251
            throw new \Exception(
252
                sprintf(
253
                    '%s has an unknown major revision',
254
                    $this->filename
255
                )
256
            );
257
        }
258
    }
259
260
    /**
261
     * Read a single integer from the current file.
262
     *
263
     * @return int
264
     */
265
    protected function readInteger()
266
    {
267
        if ($this->littleEndian) {
268
            $result = unpack('Vint', fread($this->file, 4));
269
        } else {
270
            $result = unpack('Nint', fread($this->file, 4));
271
        }
272
273
        return $result['int'];
274
    }
275
276
    /**
277
     * Read an integer from the current file.
278
     *
279
     * @param int $num
280
     * @return array
281
     */
282
    protected function readIntegerList($num)
283
    {
284
        if ($this->littleEndian) {
285
            return unpack('V' . $num, fread($this->file, 4 * $num));
286
        }
287
288
        return unpack('N' . $num, fread($this->file, 4 * $num));
289
    }
290
}
291