Passed
Pull Request — master (#8)
by Moln
05:05
created

JsonBinaryDecoderService::parseArrayOrObject()   C

Complexity

Conditions 12
Paths 38

Size

Total Lines 64
Code Lines 37

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 156

Importance

Changes 0
Metric Value
cc 12
eloc 37
c 0
b 0
f 0
nc 38
nop 2
dl 0
loc 64
ccs 0
cts 40
cp 0
crap 156
rs 6.9666

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
declare(strict_types=1);
3
4
namespace MySQLReplication\JsonBinaryDecoder;
5
6
use InvalidArgumentException;
7
use LengthException;
8
use MySQLReplication\BinaryDataReader\BinaryDataReader;
9
use MySQLReplication\BinaryDataReader\BinaryDataReaderException;
10
11
/**
12
 * @see https://github.com/mysql/mysql-server/blob/5.7/sql/json_binary.cc
13
 * @see https://github.com/mysql/mysql-server/blob/8.0/sql/json_binary.cc
14
 * @see https://github.com/shyiko/mysql-binlog-connector-java/blob/master/src/main/java/com/github/shyiko/mysql/binlog/event/deserialization/json/JsonBinary.java
15
 */
16
class JsonBinaryDecoderService
17
{
18
    public const SMALL_OBJECT = 0;
19
    public const LARGE_OBJECT = 1;
20
    public const SMALL_ARRAY = 2;
21
    public const LARGE_ARRAY = 3;
22
    public const LITERAL = 4;
23
    public const INT16 = 5;
24
    public const UINT16 = 6;
25
    public const INT32 = 7;
26
    public const UINT32 = 8;
27
    public const INT64 = 9;
28
    public const UINT64 = 10;
29
    public const DOUBLE = 11;
30
    public const STRING = 12;
31
    //public const OPAQUE = 15;
32
33
    public const LITERAL_NULL = 0;
34
    public const LITERAL_TRUE = 1;
35
    public const LITERAL_FALSE = 2;
36
37
    public const SMALL_OFFSET_SIZE = 2;
38
    public const LARGE_OFFSET_SIZE = 4;
39
40
    public const KEY_ENTRY_SIZE_SMALL = 2 + self::SMALL_OFFSET_SIZE;
41
    public const KEY_ENTRY_SIZE_LARGE = 2 + self::LARGE_OFFSET_SIZE;
42
43
    public const VALUE_ENTRY_SIZE_SMALL = 1 + self::SMALL_OFFSET_SIZE;
44
    public const VALUE_ENTRY_SIZE_LARGE = 1 + self::LARGE_OFFSET_SIZE;
45
46
    public const OBJECT = 1;
47
    public const ARRAY = 2;
48
    public const SCALAR = 3;
49
50
    private $binaryDataReader;
51
    private $jsonBinaryDecoderFormatter;
52
    private $dataLength;
53
54
    public function __construct(
55
        BinaryDataReader           $binaryDataReader,
56
        JsonBinaryDecoderFormatter $jsonBinaryDecoderFormatter
57
    ) {
58
        $this->binaryDataReader = $binaryDataReader;
59
        $this->jsonBinaryDecoderFormatter = $jsonBinaryDecoderFormatter;
60
        $this->dataLength = $this->binaryDataReader->getBinaryDataLength();
61
    }
62
63
    public static function makeJsonBinaryDecoder(string $data): self
64
    {
65
        return new self(
66
            new BinaryDataReader($data),
67
            new JsonBinaryDecoderFormatter()
68
        );
69
    }
70
71
    /**
72
     * @throws BinaryDataReaderException
73
     * @throws JsonBinaryDecoderException
74
     */
75
    public function parseToString(): string
76
    {
77
        $this->parseJson($this->binaryDataReader->readUInt8());
78
79
        return $this->jsonBinaryDecoderFormatter->getJsonString();
80
    }
81
82
    /**
83
     * @throws JsonBinaryDecoderException
84
     * @throws BinaryDataReaderException
85
     */
86
    private function parseJson(int $type): void
87
    {
88
        $results = [];
89
        if (self::SMALL_OBJECT === $type) {
90
            $results[self::OBJECT] = $this->parseArrayOrObject(self::OBJECT, self::SMALL_OFFSET_SIZE);
91
        } elseif (self::LARGE_OBJECT === $type) {
92
            $results[self::OBJECT] = $this->parseArrayOrObject(self::OBJECT, self::LARGE_OFFSET_SIZE);
93
        } elseif (self::SMALL_ARRAY === $type) {
94
            $results[self::ARRAY] = $this->parseArrayOrObject(self::ARRAY, self::SMALL_OFFSET_SIZE);
95
        } elseif (self::LARGE_ARRAY === $type) {
96
            $results[self::ARRAY] = $this->parseArrayOrObject(self::ARRAY, self::LARGE_OFFSET_SIZE);
97
        } else {
98
            $results[self::SCALAR][] = [
99
                'name' => null,
100
                'value' => $this->parseScalar($type)
101
            ];
102
        }
103
104
        $this->parseToJson($results);
105
    }
106
107
    /**
108
     * @throws BinaryDataReaderException
109
     * @throws JsonBinaryDecoderException
110
     */
111
    private function parseToJson(array $results): void
112
    {
113
        foreach ($results as $dataType => $entities) {
114
            if (self::OBJECT === $dataType) {
115
                $this->jsonBinaryDecoderFormatter->formatBeginObject();
116
            } elseif (self::ARRAY === $dataType) {
117
                $this->jsonBinaryDecoderFormatter->formatBeginArray();
118
            }
119
120
            foreach ($entities as $i => $entity) {
121
                if ($dataType === self::SCALAR) {
122
                    if (null === $entity['value']->getValue()) {
123
                        $this->jsonBinaryDecoderFormatter->formatValue('null');
124
                    } elseif (is_bool($entity['value']->getValue())) {
125
                        $this->jsonBinaryDecoderFormatter->formatValueBool($entity['value']->getValue());
126
                    } else {
127
                        $this->jsonBinaryDecoderFormatter->formatValue($entity['value']->getValue());
128
                    }
129
                    continue;
130
                }
131
132
133
                if ($i !== 0) {
134
                    $this->jsonBinaryDecoderFormatter->formatNextEntry();
135
                }
136
137
                if (null !== $entity['name']) {
138
                    $this->jsonBinaryDecoderFormatter->formatName($entity['name']);
139
                }
140
                $this->assignValues($entity['value']);
141
            }
142
143
            if (self::OBJECT === $dataType) {
144
                $this->jsonBinaryDecoderFormatter->formatEndObject();
145
            } elseif (self::ARRAY === $dataType) {
146
                $this->jsonBinaryDecoderFormatter->formatEndArray();
147
            }
148
        }
149
    }
150
151
    /**
152
     * @throws BinaryDataReaderException
153
     * @throws JsonBinaryDecoderException
154
     */
155
    private function parseArrayOrObject(int $type, int $intSize): array
156
    {
157
        $large = $intSize === self::LARGE_OFFSET_SIZE;
158
        $offsetSize = self::offsetSize($large);
159
        if ($this->dataLength < 2 * $offsetSize) {
160
            throw new InvalidArgumentException('Document is not long enough to contain the two length fields');
161
        }
162
163
        $elementCount = $this->binaryDataReader->readUIntBySize($intSize);
164
        $bytes = $this->binaryDataReader->readUIntBySize($intSize);
165
166
        if ($bytes > $this->dataLength) {
167
            throw new InvalidArgumentException(
168
                'The value can\'t have more bytes than what\'s available in the data buffer.'
169
            );
170
        }
171
172
        $keyEntrySize = self::keyEntrySize($large);
173
        $valueEntrySize = self::valueEntrySize($large);
174
175
        $headerSize = 2 * $offsetSize;
176
177
        if ($type === self::OBJECT) {
178
            $headerSize += $elementCount * $keyEntrySize;
179
        }
180
        $headerSize += $elementCount * $valueEntrySize;
181
182
        if ($headerSize > $bytes) {
183
            throw new InvalidArgumentException('Header is larger than the full size of the value.');
184
        }
185
186
        $keyLengths = [];
187
        if ($type === self::OBJECT) {
188
            // Read each key-entry, consisting of the offset and length of each key ...
189
            for ($i = 0; $i !== $elementCount; ++$i) {
190
                $keyOffset = $this->binaryDataReader->readUIntBySize($intSize);
191
                $keyLengths[$i] = $this->binaryDataReader->readUInt16();
192
                if ($keyOffset < $headerSize) {
193
                    throw new InvalidArgumentException('Invalid key offset');
194
                }
195
            }
196
        }
197
198
        $entries = [];
199
        for ($i = 0; $i !== $elementCount; ++$i) {
200
            $entries[$i] = $this->getOffsetOrInLinedValue($bytes, $intSize);
201
        }
202
203
        $keys = [];
204
        if ($type === self::OBJECT) {
205
            for ($i = 0; $i !== $elementCount; ++$i) {
206
                $keys[$i] = $this->binaryDataReader->read($keyLengths[$i]);
207
            }
208
        }
209
210
        $results = [];
211
        for ($i = 0; $i !== $elementCount; ++$i) {
212
            $results[] = [
213
                'name' => $keys[$i] ?? null,
214
                'value' => $entries[$i]
215
            ];
216
        }
217
218
        return $results;
219
    }
220
221
    private static function offsetSize(bool $large): int
222
    {
223
        return $large ? self::LARGE_OFFSET_SIZE : self::SMALL_OFFSET_SIZE;
224
    }
225
226
    private static function keyEntrySize(bool $large): int
227
    {
228
        return $large ? self::KEY_ENTRY_SIZE_LARGE : self::KEY_ENTRY_SIZE_SMALL;
229
    }
230
231
    private static function valueEntrySize(bool $large): int
232
    {
233
        return $large ? self::VALUE_ENTRY_SIZE_LARGE : self::VALUE_ENTRY_SIZE_SMALL;
234
    }
235
236
    /**
237
     * @throws BinaryDataReaderException
238
     * @throws JsonBinaryDecoderException
239
     */
240
    private function getOffsetOrInLinedValue(int $bytes, int $intSize): JsonBinaryDecoderValue
241
    {
242
        $type = $this->binaryDataReader->readUInt8();
243
        if (self::isInLinedType($type, $intSize)) {
244
            return $this->parseScalar($type);
245
        }
246
247
        $offset = $this->binaryDataReader->readUIntBySize($intSize);
248
        if ($offset > $bytes) {
249
            throw new LengthException(
250
                'The offset for the value in the JSON binary document is ' . $offset . ', which is larger than the binary form of the JSON document (' . $bytes . ' bytes)'
251
            );
252
        }
253
254
        return new JsonBinaryDecoderValue(false, null, $type, $offset);
255
    }
256
257
    private static function isInLinedType(int $type, int $intSize): bool
258
    {
259
        switch ($type) {
260
            case self::LITERAL:
261
            case self::INT16:
262
            case self::UINT16:
263
                return true;
264
            case self::INT32:
265
            case self::UINT32:
266
                return self::LARGE_OFFSET_SIZE === $intSize;
267
            default:
268
                return false;
269
        }
270
    }
271
272
    /**
273
     * @throws JsonBinaryDecoderException
274
     */
275
    private function parseScalar(int $type): JsonBinaryDecoderValue
276
    {
277
        if (self::LITERAL === $type) {
278
            $data = $this->readLiteral();
279
        } elseif (self::INT16 === $type) {
280
            $data = $this->binaryDataReader->readInt16();
281
        } elseif (self::INT32 === $type) {
282
            $data = ($this->binaryDataReader->readInt32());
283
        } elseif (self::INT64 === $type) {
284
            $data = $this->binaryDataReader->readInt64();
285
        } elseif (self::UINT16 === $type) {
286
            $data = ($this->binaryDataReader->readUInt16());
287
        } elseif (self::UINT64 === $type) {
288
            $data = ($this->binaryDataReader->readUInt64());
289
        } elseif (self::DOUBLE === $type) {
290
            $data = ($this->binaryDataReader->readDouble());
291
        } elseif (self::STRING === $type) {
292
            $data = ($this->binaryDataReader->read($this->readVariableInt()));
293
        // } else if (self::OPAQUE === $type) {
294
        } else {
295
            throw new JsonBinaryDecoderException(
296
                JsonBinaryDecoderException::UNKNOWN_JSON_TYPE_MESSAGE . $type,
297
                JsonBinaryDecoderException::UNKNOWN_JSON_TYPE_CODE
298
            );
299
        }
300
301
        return new JsonBinaryDecoderValue(true, $data, $type);
302
    }
303
304
    private function readLiteral(): ?bool
305
    {
306
        $literal = ord($this->binaryDataReader->read(BinaryDataReader::UNSIGNED_SHORT_LENGTH));
307
        if (self::LITERAL_NULL === $literal) {
308
            return null;
309
        }
310
        if (self::LITERAL_TRUE === $literal) {
311
            return true;
312
        }
313
        if (self::LITERAL_FALSE === $literal) {
314
            return false;
315
        }
316
317
        return null;
318
    }
319
320
    private function readVariableInt(): int
321
    {
322
        $maxBytes = min($this->binaryDataReader->getBinaryDataLength(), 5);
323
        $len = 0;
324
        for ($i = 0; $i < $maxBytes; ++$i) {
325
            $size = $this->binaryDataReader->readUInt8();
326
            // Get the next 7 bits of the length.
327
            $len |= ($size & 0x7f) << (7 * $i);
328
            if (($size & 0x80) === 0) {
329
                // This was the last byte. Return successfully.
330
                return $len;
331
            }
332
        }
333
334
        return $len;
335
    }
336
337
    /**
338
     * @throws JsonBinaryDecoderException
339
     * @throws BinaryDataReaderException
340
     */
341
    private function assignValues(JsonBinaryDecoderValue $jsonBinaryDecoderValue): void
342
    {
343
        if (false === $jsonBinaryDecoderValue->isIsResolved()) {
344
            $this->ensureOffset($jsonBinaryDecoderValue->getOffset());
345
            $this->parseJson($jsonBinaryDecoderValue->getType());
346
        } elseif (null === $jsonBinaryDecoderValue->getValue()) {
347
            $this->jsonBinaryDecoderFormatter->formatValueNull();
348
        } elseif (is_bool($jsonBinaryDecoderValue->getValue())) {
349
            $this->jsonBinaryDecoderFormatter->formatValueBool($jsonBinaryDecoderValue->getValue());
350
        } elseif (is_numeric($jsonBinaryDecoderValue->getValue())) {
351
            $this->jsonBinaryDecoderFormatter->formatValueNumeric($jsonBinaryDecoderValue->getValue());
352
        }
353
    }
354
355
    private function ensureOffset(?int $ensureOffset): void
356
    {
357
        if (null === $ensureOffset) {
358
            return;
359
        }
360
        $pos = $this->binaryDataReader->getReadBytes();
361
        if ($pos !== $ensureOffset) {
362
            if ($ensureOffset < $pos) {
363
                return;
364
            }
365
            $this->binaryDataReader->advance($ensureOffset + 1 - $pos);
366
        }
367
    }
368
}
369