Passed
Push — master ( 064a98...723530 )
by kacper
04:52
created

JsonBinaryDecoderService   F

Complexity

Total Complexity 72

Size/Duplication

Total Lines 355
Duplicated Lines 0 %

Test Coverage

Coverage 0%

Importance

Changes 4
Bugs 0 Features 0
Metric Value
eloc 184
dl 0
loc 355
ccs 0
cts 243
cp 0
rs 2.64
c 4
b 0
f 0
wmc 72

16 Methods

Rating   Name   Duplication   Size   Complexity  
A readLiteral() 0 14 4
A getOffsetOrInLinedValue() 0 15 3
A parseToString() 0 5 1
A valueEntrySize() 0 3 2
A keyEntrySize() 0 3 2
A isInLinedType() 0 12 6
A ensureOffset() 0 11 4
A parseJson() 0 18 5
A __construct() 0 7 1
B parseScalar() 0 32 9
A assignValues() 0 11 5
C parseToJson() 0 37 12
A readVariableInt() 0 15 3
C parseArrayOrObject() 0 64 12
A makeJsonBinaryDecoder() 0 5 1
A offsetSize() 0 3 2

How to fix   Complexity   

Complex Class

Complex classes like JsonBinaryDecoderService often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use JsonBinaryDecoderService, and based on these observations, apply Extract Interface, too.

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
        if (self::SMALL_OBJECT === $type) {
89
            $results[self::OBJECT] = $this->parseArrayOrObject(self::OBJECT, self::SMALL_OFFSET_SIZE);
0 ignored issues
show
Comprehensibility Best Practice introduced by
$results was never initialized. Although not strictly required by PHP, it is generally a good practice to add $results = array(); before regardless.
Loading history...
90
        } else if (self::LARGE_OBJECT === $type) {
91
            $results[self::OBJECT] = $this->parseArrayOrObject(self::OBJECT, self::LARGE_OFFSET_SIZE);
92
        } else if (self::SMALL_ARRAY === $type) {
93
            $results[self::ARRAY] = $this->parseArrayOrObject(self::ARRAY, self::SMALL_OFFSET_SIZE);
94
        } else if (self::LARGE_ARRAY === $type) {
95
            $results[self::ARRAY] = $this->parseArrayOrObject(self::ARRAY, self::LARGE_OFFSET_SIZE);
96
        } else {
97
            $results[self::SCALAR][] = [
98
                'name' => null,
99
                'value' => $this->parseScalar($type)
100
            ];
101
        }
102
103
        $this->parseToJson($results);
104
    }
105
106
    /**
107
     * @throws BinaryDataReaderException
108
     * @throws JsonBinaryDecoderException
109
     */
110
    private function parseToJson(array $results): void
111
    {
112
        foreach ($results as $dataType => $entities) {
113
            if (self::OBJECT === $dataType) {
114
                $this->jsonBinaryDecoderFormatter->formatBeginObject();
115
            } else if (self::ARRAY === $dataType) {
116
                $this->jsonBinaryDecoderFormatter->formatBeginArray();
117
            }
118
119
            foreach ($entities as $i => $entity) {
120
                if ($dataType === self::SCALAR) {
121
122
                    if (null === $entity['value']->getValue()) {
123
                        $this->jsonBinaryDecoderFormatter->formatValue('null');
124
                    } else if (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
            } else if (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
        } else if (self::INT16 === $type) {
280
            $data = $this->binaryDataReader->readInt16();
281
        } else if (self::INT32 === $type) {
282
            $data = ($this->binaryDataReader->readInt32());
283
        } else if (self::INT64 === $type) {
284
            $data = $this->binaryDataReader->readInt64();
285
        } else if (self::UINT16 === $type) {
286
            $data = ($this->binaryDataReader->readUInt16());
287
        } else if (self::UINT64 === $type) {
288
            $data = ($this->binaryDataReader->readUInt64());
289
        } else if (self::DOUBLE === $type) {
290
            $data = ($this->binaryDataReader->readDouble());
291
        } else if (self::STRING === $type) {
292
            $data = ($this->binaryDataReader->read($this->readVariableInt()));
293
        } /**
294
         * else if (self::OPAQUE === $type)
295
         * {
296
         *
297
         * }
298
         */
299
        else {
300
            throw new JsonBinaryDecoderException(
301
                JsonBinaryDecoderException::UNKNOWN_JSON_TYPE_MESSAGE . $type,
302
                JsonBinaryDecoderException::UNKNOWN_JSON_TYPE_CODE
303
            );
304
        }
305
306
        return new JsonBinaryDecoderValue(true, $data, $type);
307
    }
308
309
    private function readLiteral(): ?bool
310
    {
311
        $literal = ord($this->binaryDataReader->read(BinaryDataReader::UNSIGNED_SHORT_LENGTH));
312
        if (self::LITERAL_NULL === $literal) {
313
            return null;
314
        }
315
        if (self::LITERAL_TRUE === $literal) {
316
            return true;
317
        }
318
        if (self::LITERAL_FALSE === $literal) {
319
            return false;
320
        }
321
322
        return null;
323
    }
324
325
    private function readVariableInt(): int
326
    {
327
        $maxBytes = min($this->binaryDataReader->getBinaryDataLength(), 5);
328
        $len = 0;
329
        for ($i = 0; $i < $maxBytes; ++$i) {
330
            $size = $this->binaryDataReader->readUInt8();
331
            // Get the next 7 bits of the length.
332
            $len |= ($size & 0x7f) << (7 * $i);
333
            if (($size & 0x80) === 0) {
334
                // This was the last byte. Return successfully.
335
                return $len;
336
            }
337
        }
338
339
        return $len;
340
    }
341
342
    /**
343
     * @throws JsonBinaryDecoderException
344
     * @throws BinaryDataReaderException
345
     */
346
    private function assignValues(JsonBinaryDecoderValue $jsonBinaryDecoderValue): void
347
    {
348
        if (false === $jsonBinaryDecoderValue->isIsResolved()) {
349
            $this->ensureOffset($jsonBinaryDecoderValue->getOffset());
350
            $this->parseJson($jsonBinaryDecoderValue->getType());
351
        } else if (null === $jsonBinaryDecoderValue->getValue()) {
352
            $this->jsonBinaryDecoderFormatter->formatValueNull();
353
        } else if (is_bool($jsonBinaryDecoderValue->getValue())) {
354
            $this->jsonBinaryDecoderFormatter->formatValueBool($jsonBinaryDecoderValue->getValue());
355
        } else if (is_numeric($jsonBinaryDecoderValue->getValue())) {
356
            $this->jsonBinaryDecoderFormatter->formatValueNumeric($jsonBinaryDecoderValue->getValue());
357
        }
358
    }
359
360
    private function ensureOffset(?int $ensureOffset): void
361
    {
362
        if (null === $ensureOffset) {
363
            return;
364
        }
365
        $pos = $this->binaryDataReader->getReadBytes();
366
        if ($pos !== $ensureOffset) {
367
            if ($ensureOffset < $pos) {
368
                return;
369
            }
370
            $this->binaryDataReader->advance($ensureOffset + 1 - $pos);
371
        }
372
    }
373
}