Completed
Push — master ( dd1269...21c3dd )
by Sam
02:11
created

Data::readOpcode()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 2

Importance

Changes 0
Metric Value
cc 2
eloc 4
c 0
b 0
f 0
nc 2
nop 1
dl 0
loc 8
ccs 5
cts 5
cp 1
crap 2
rs 10
1
<?php
2
3
namespace SPSS\Sav\Record;
4
5
use SPSS\Buffer;
6
use SPSS\Exception;
7
use SPSS\Sav\Record;
8
use SPSS\Utils;
9
10
class Data extends Record
11
{
12
    const TYPE = 999;
13
14
    /** No-operation. This is simply ignored. */
15
    const OPCODE_NOP = 0;
16
17
    /** End-of-file. */
18
    const OPCODE_EOF = 252;
19
20
    /** Verbatim raw data. Read an 8-byte segment of raw data. */
21
    const OPCODE_RAW_DATA = 253;
22
23
    /** Compressed whitespaces. Expand to an 8-byte segment of whitespaces. */
24
    const OPCODE_WHITESPACES = 254;
25
26
    /** Compressed sysmiss value. Expand to an 8-byte segment of SYSMISS value. */
27
    const OPCODE_SYSMISS = 255;
28
29
    /**
30
     * @var array [case_index][var_index]
31
     */
32
    public $matrix = [];
33
34
    /**
35
     * @var array Latest opcodes data
36
     */
37
    private $opcodes = [];
38
39
    /**
40
     * @var int Current opcode index
41
     */
42
    private $opcodeIndex = 0;
43
44
    /**
45
     * @param Buffer $buffer
46
     * @throws Exception
47
     */
48 5
    public function read(Buffer $buffer)
49
    {
50 5
        if ($buffer->readInt() != 0) {
51
            throw new \InvalidArgumentException('Error reading data record. Non-zero value found.');
52
        }
53 5
        if (! isset($buffer->context->variables)) {
54
            throw new \InvalidArgumentException('Variables required');
55
        }
56 5
        if (! isset($buffer->context->header)) {
57
            throw new \InvalidArgumentException('Header required');
58
        }
59 5
        if (! isset($buffer->context->info)) {
60
            throw new \InvalidArgumentException('Info required');
61
        }
62
63 5
        $compressed = $buffer->context->header->compression;
64 5
        $bias = $buffer->context->header->bias;
65 5
        $casesCount = $buffer->context->header->casesCount;
66
67
        /** @var Variable[] $variables */
68 5
        $variables = $buffer->context->variables;
69
70
        /** @var Record\Info[] $info */
71 5
        $info = $buffer->context->info;
72
73 5
        $veryLongStrings = [];
74 5
        if (isset($info[Record\Info\VeryLongString::SUBTYPE])) {
75 1
            $veryLongStrings = $info[Record\Info\VeryLongString::SUBTYPE]->toArray();
76
        }
77
78 5
        if (isset($info[Record\Info\MachineFloatingPoint::SUBTYPE])) {
79 5
            $sysmis = $info[Record\Info\MachineFloatingPoint::SUBTYPE]->sysmis;
80
        } else {
81
            $sysmis = NAN;
82
        }
83
84 5
        $this->opcodeIndex = 8;
85
86 5
        for ($case = 0; $case < $casesCount; $case++) {
87 4
            $parent = -1;
0 ignored issues
show
Unused Code introduced by
The assignment to $parent is dead and can be removed.
Loading history...
88 4
            $octs = 0;
89 4
            $varCount = count($variables);
90 4
            $varNum = 0;
91 4
            for($index = 0; $index < $varCount; $index++) {
92 4
                $var = $variables[$index];
93 4
                $isNumeric = $var->width == 0;
94 4
                $width = isset($var->write[2]) ? $var->write[2] : $var->width;
95
96 4
                if ($isNumeric) {
97 3
                    if (! $compressed) {
98
                        $this->matrix[$case][$varNum] = $buffer->readDouble();
99
                    } else {
100 3
                        $opcode = $this->readOpcode($buffer);
101
                        switch ($opcode) {
102 3
                            case self::OPCODE_NOP;
103
                                break;
104 3
                            case self::OPCODE_EOF;
105
                                throw new Exception(
106
                                    'Error reading data: unexpected end of compressed data file (cluster code 252)'
107
                                );
108
                                break;
109 3
                            case self::OPCODE_RAW_DATA;
110 2
                                $this->matrix[$case][$varNum] = $buffer->readDouble();
111 2
                                break;
112 3
                            case self::OPCODE_SYSMISS;
113
                                $this->matrix[$case][$varNum] = $sysmis;
114
                                break;
115
                            default:
116 3
                                $this->matrix[$case][$varNum] = $opcode - $bias;
117 3
                                break;
118
                        }
119
                    }
120
                } else {
121 3
                    $width = isset($veryLongStrings[$var->name]) ? $veryLongStrings[$var->name] : $width;
122 3
                    $this->matrix[$case][$varNum] = '';
123 3
                    $segmentsCount = Utils::widthToSegments($width);
124 3
                    $opcode = self::OPCODE_RAW_DATA;
125 3
                    $index = $index - 1;
126 3
                    for ($s = 0; $s < $segmentsCount; $s++) {
127 3
                        $segWidth = Utils::segmentAllocWidth($width, $s);
128 3
                        $octs = Utils::widthToOcts($segWidth);
129 3
                        $index = $index + $octs;    // Skip a few variables for this segment
130 3
                        if ($opcode === self::OPCODE_NOP || $opcode === self::OPCODE_EOF) {
131
                            // If next segments are empty too, skip
132
                            $continue;
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $continue seems to be never defined.
Loading history...
133
                        }
134 3
                        for ($i = $segWidth; $i > 0; $i -= 8) {
135 3
                            if ($segWidth == 255) {
136 1
                                $chunkSize = min($i, 8);
0 ignored issues
show
Unused Code introduced by
The assignment to $chunkSize is dead and can be removed.
Loading history...
137
                            } else {
138 3
                                $chunkSize = 8;
139
                            }
140
141 3
                            $val = '';
142 3
                            if (! $compressed) {
143
                                $val = $buffer->readString(8);
144
                            } else {
145 3
                                $opcode = $this->readOpcode($buffer);
146
                                switch ($opcode) {
147 3
                                    case self::OPCODE_NOP;
148
                                        break 2;
149 3
                                    case self::OPCODE_EOF;
150
                                        throw new Exception(
151
                                            'Error reading data: unexpected end of compressed data file (cluster code 252)'
152
                                        );
153
                                        break 2;
154 3
                                    case self::OPCODE_RAW_DATA;
155 3
                                        $val = $buffer->readString(8);
156 3
                                        break;
157 3
                                    case self::OPCODE_WHITESPACES;
158 3
                                        $val = '        ';
159 3
                                        break;
160
                                }
161
                            }
162 3
                            $this->matrix[$case][$varNum] .= $val;
163
                        }
164 3
                        $this->matrix[$case][$varNum] = rtrim($this->matrix[$case][$varNum]);
165
                    }
166
                }
167 4
                $varNum++;
168
            }
169
        }
170 5
    }
171
172
    /**
173
     * @param Buffer $buffer
174
     * @return int
175
     */
176 4
    public function readOpcode(Buffer $buffer)
177
    {
178 4
        if ($this->opcodeIndex >= 8) {
179 4
            $this->opcodes = $buffer->readBytes(8);
0 ignored issues
show
Documentation Bug introduced by
It seems like $buffer->readBytes(8) can also be of type false. However, the property $opcodes is declared as type array. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
180 4
            $this->opcodeIndex = 0;
181
        }
182
183 4
        return 0xFF & $this->opcodes[$this->opcodeIndex++];
184
    }
185
186
    /**
187
     * @param Buffer $buffer
188
     */
189 5
    public function write(Buffer $buffer)
190
    {
191 5
        $buffer->writeInt(self::TYPE);
192 5
        $buffer->writeInt(0);
193
194 5
        if (! isset($buffer->context->variables)) {
195
            throw new \InvalidArgumentException('Variables required');
196
        }
197 5
        if (! isset($buffer->context->header)) {
198
            throw new \InvalidArgumentException('Header required');
199
        }
200 5
        if (! isset($buffer->context->info)) {
201
            throw new \InvalidArgumentException('Info required');
202
        }
203
204 5
        $compressed = $buffer->context->header->compression;
205 5
        $bias = $buffer->context->header->bias;
206 5
        $casesCount = $buffer->context->header->casesCount;
207
208
        /** @var Variable[] $variables */
209 5
        $variables = $buffer->context->variables;
210
211
        /** @var Record\Info[] $info */
212 5
        $info = $buffer->context->info;
213
214 5
        $veryLongStrings = [];
215 5
        if (isset($info[Record\Info\VeryLongString::SUBTYPE])) {
216 5
            $veryLongStrings = $info[Record\Info\VeryLongString::SUBTYPE]->toArray();
217
        }
218
219 5
        if (isset($info[Record\Info\MachineFloatingPoint::SUBTYPE])) {
220 5
            $sysmis = $info[Record\Info\MachineFloatingPoint::SUBTYPE]->sysmis;
221
        } else {
222
            $sysmis = NAN;
223
        }
224
225 5
        $dataBuffer = Buffer::factory('', ['memory' => true]);
226
227 5
        for ($case = 0; $case < $casesCount; $case++) {
228 4
            foreach ($variables as $index => $var) {
229 4
                $value = $this->matrix[$case][$index];
230
231
                // $isNumeric = $var->width == 0;
232 4
                $isNumeric = $var->width == 0 && \SPSS\Sav\Variable::isNumberFormat($var->write[1]);
233 4
                $width = isset($var->write[2]) ? $var->write[2] : $var->width;
234
235 4
                if ($isNumeric) {
236 2
                    if (! $compressed) {
237
                        $buffer->writeDouble($value);
238
                    } else {
239 2
                        if ($value == $sysmis || $value == "") {
240
                            $this->writeOpcode($buffer, $dataBuffer, self::OPCODE_SYSMISS);
241 2
                        } elseif ($value >= 1 - $bias && $value <= 251 - $bias && $value == (int) $value) {
242 1
                            $this->writeOpcode($buffer, $dataBuffer, $value + $bias);
243
                        } else {
244 1
                            $this->writeOpcode($buffer, $dataBuffer, self::OPCODE_RAW_DATA);
245 2
                            $dataBuffer->writeDouble($value);
246
                        }
247
                    }
248
                } else {
249 4
                    if (! $compressed) {
250
                        $buffer->writeString($value, Utils::roundUp($width, 8));
251
                    } else {
252 4
                        $offset = 0;
253 4
                        $width = isset($veryLongStrings[$var->name]) ? $veryLongStrings[$var->name] : $width;
254 4
                        $segmentsCount = Utils::widthToSegments($width);
255 4
                        for ($s = 0; $s < $segmentsCount; $s++) {
256 4
                            $segWidth = Utils::segmentAllocWidth($width, $s);
257 4
                            for ($i = $segWidth; $i > 0; $i -= 8) {
258 4
                                if ($segWidth == 255) {
259 1
                                    $chunkSize = min($i, 8);
260
                                } else {
261 4
                                    $chunkSize = 8;
262
                                }
263 4
                                $val = substr($value, $offset, $chunkSize);  // Read 8 byte segements, don't use mbsubstr here
264 4
                                if ($val == "") {
265 4
                                    $this->writeOpcode($buffer, $dataBuffer, self::OPCODE_WHITESPACES);
266
                                } else {
267 4
                                    $this->writeOpcode($buffer, $dataBuffer, self::OPCODE_RAW_DATA);
268 4
                                    $dataBuffer->writeString($val, 8);
269
                                }
270 4
                                $offset += $chunkSize;
271
                            }
272
                        }
273
                    }
274
                }
275
            }
276
        }
277
278 5
        $this->writeOpcode($buffer, $dataBuffer, self::OPCODE_EOF);
279 5
    }
280
281
    /**
282
     * @param Buffer $buffer
283
     * @param Buffer $dataBuffer
284
     * @param int $opcode
285
     */
286 5
    public function writeOpcode(Buffer $buffer, Buffer $dataBuffer, $opcode)
287
    {
288 5
        if ($this->opcodeIndex >= 8 || $opcode == self::OPCODE_EOF) {
289 5
            foreach ($this->opcodes as $opc) {
290 4
                $buffer->write(chr($opc));
291
            }
292 5
            $padding = max(8 - count($this->opcodes), 0);
293 5
            for ($i = 0; $i < $padding; $i++) {
294 4
                $buffer->write(chr(self::OPCODE_NOP));
295
            }
296 5
            $this->opcodes = [];
297 5
            $this->opcodeIndex = 0;
298 5
            $dataBuffer->rewind();
299 5
            $buffer->writeStream($dataBuffer->getStream());
300 5
            $dataBuffer->truncate();
301
        }
302 5
        $this->opcodes[$this->opcodeIndex++] = 0xFF & $opcode;
303 5
    }
304
305
    /**
306
     * @return array
307
     */
308 5
    public function toArray()
309
    {
310 5
        return $this->matrix;
311
    }
312
}
313