JsonBinaryDecoderService   F
last analyzed

Complexity

Total Complexity 75

Size/Duplication

Total Lines 371
Duplicated Lines 0 %

Test Coverage

Coverage 0%

Importance

Changes 5
Bugs 0 Features 0
Metric Value
eloc 191
c 5
b 0
f 0
dl 0
loc 371
ccs 0
cts 162
cp 0
rs 2.4
wmc 75

16 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 7 1
A makeJsonBinaryDecoder() 0 5 1
A readLiteral() 0 14 4
A getOffsetOrInLinedValue() 0 25 5
A parseToString() 0 10 2
A valueEntrySize() 0 3 2
A keyEntrySize() 0 3 2
A isInLinedType() 0 12 6
A ensureOffset() 0 11 4
A parseJson() 0 19 5
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 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
        // Sometimes, we can insert a NULL JSON even we set the JSON field as NOT NULL.
78
        // If we meet this case, we can return a 'null' value.
79
        if($this->binaryDataReader->getBinaryDataLength() === 0) {
80
            return 'null';
81
        }
82
        $this->parseJson($this->binaryDataReader->readUInt8());
83
84
        return $this->jsonBinaryDecoderFormatter->getJsonString();
85
    }
86
87
    /**
88
     * @throws JsonBinaryDecoderException
89
     * @throws BinaryDataReaderException
90
     */
91
    private function parseJson(int $type): void
92
    {
93
        $results = [];
94
        if (self::SMALL_OBJECT === $type) {
95
            $results[self::OBJECT] = $this->parseArrayOrObject(self::OBJECT, self::SMALL_OFFSET_SIZE);
96
        } else if (self::LARGE_OBJECT === $type) {
97
            $results[self::OBJECT] = $this->parseArrayOrObject(self::OBJECT, self::LARGE_OFFSET_SIZE);
98
        } else if (self::SMALL_ARRAY === $type) {
99
            $results[self::ARRAY] = $this->parseArrayOrObject(self::ARRAY, self::SMALL_OFFSET_SIZE);
100
        } else if (self::LARGE_ARRAY === $type) {
101
            $results[self::ARRAY] = $this->parseArrayOrObject(self::ARRAY, self::LARGE_OFFSET_SIZE);
102
        } else {
103
            $results[self::SCALAR][] = [
104
                'name' => null,
105
                'value' => $this->parseScalar($type)
106
            ];
107
        }
108
109
        $this->parseToJson($results);
110
    }
111
112
    /**
113
     * @throws BinaryDataReaderException
114
     * @throws JsonBinaryDecoderException
115
     */
116
    private function parseToJson(array $results): void
117
    {
118
        foreach ($results as $dataType => $entities) {
119
            if (self::OBJECT === $dataType) {
120
                $this->jsonBinaryDecoderFormatter->formatBeginObject();
121
            } else if (self::ARRAY === $dataType) {
122
                $this->jsonBinaryDecoderFormatter->formatBeginArray();
123
            }
124
125
            foreach ($entities as $i => $entity) {
126
                if ($dataType === self::SCALAR) {
127
128
                    if (null === $entity['value']->getValue()) {
129
                        $this->jsonBinaryDecoderFormatter->formatValue('null');
130
                    } else if (is_bool($entity['value']->getValue())) {
131
                        $this->jsonBinaryDecoderFormatter->formatValueBool($entity['value']->getValue());
132
                    } else {
133
                        $this->jsonBinaryDecoderFormatter->formatValue($entity['value']->getValue());
134
                    }
135
                    continue;
136
                }
137
138
139
                if ($i !== 0) {
140
                    $this->jsonBinaryDecoderFormatter->formatNextEntry();
141
                }
142
143
                if (null !== $entity['name']) {
144
                    $this->jsonBinaryDecoderFormatter->formatName($entity['name']);
145
                }
146
                $this->assignValues($entity['value']);
147
            }
148
149
            if (self::OBJECT === $dataType) {
150
                $this->jsonBinaryDecoderFormatter->formatEndObject();
151
            } else if (self::ARRAY === $dataType) {
152
                $this->jsonBinaryDecoderFormatter->formatEndArray();
153
            }
154
        }
155
    }
156
157
    /**
158
     * @throws BinaryDataReaderException
159
     * @throws JsonBinaryDecoderException
160
     */
161
    private function parseArrayOrObject(int $type, int $intSize): array
162
    {
163
        $large = $intSize === self::LARGE_OFFSET_SIZE;
164
        $offsetSize = self::offsetSize($large);
165
        if ($this->dataLength < 2 * $offsetSize) {
166
            throw new InvalidArgumentException('Document is not long enough to contain the two length fields');
167
        }
168
169
        $elementCount = $this->binaryDataReader->readUIntBySize($intSize);
170
        $bytes = $this->binaryDataReader->readUIntBySize($intSize);
171
172
        if ($bytes > $this->dataLength) {
173
            throw new InvalidArgumentException(
174
                'The value can\'t have more bytes than what\'s available in the data buffer.'
175
            );
176
        }
177
178
        $keyEntrySize = self::keyEntrySize($large);
179
        $valueEntrySize = self::valueEntrySize($large);
180
181
        $headerSize = 2 * $offsetSize;
182
183
        if ($type === self::OBJECT) {
184
            $headerSize += $elementCount * $keyEntrySize;
185
        }
186
        $headerSize += $elementCount * $valueEntrySize;
187
188
        if ($headerSize > $bytes) {
189
            throw new InvalidArgumentException('Header is larger than the full size of the value.');
190
        }
191
192
        $keyLengths = [];
193
        if ($type === self::OBJECT) {
194
            // Read each key-entry, consisting of the offset and length of each key ...
195
            for ($i = 0; $i !== $elementCount; ++$i) {
196
                $keyOffset = $this->binaryDataReader->readUIntBySize($intSize);
197
                $keyLengths[$i] = $this->binaryDataReader->readUInt16();
198
                if ($keyOffset < $headerSize) {
199
                    throw new InvalidArgumentException('Invalid key offset');
200
                }
201
            }
202
        }
203
204
        $entries = [];
205
        for ($i = 0; $i !== $elementCount; ++$i) {
206
            $entries[$i] = $this->getOffsetOrInLinedValue($bytes, $intSize, $valueEntrySize);
207
        }
208
209
        $keys = [];
210
        if ($type === self::OBJECT) {
211
            for ($i = 0; $i !== $elementCount; ++$i) {
212
                $keys[$i] = $this->binaryDataReader->read($keyLengths[$i]);
213
            }
214
        }
215
216
        $results = [];
217
        for ($i = 0; $i !== $elementCount; ++$i) {
218
            $results[] = [
219
                'name' => $keys[$i] ?? null,
220
                'value' => $entries[$i]
221
            ];
222
        }
223
224
        return $results;
225
    }
226
227
    private static function offsetSize(bool $large): int
228
    {
229
        return $large ? self::LARGE_OFFSET_SIZE : self::SMALL_OFFSET_SIZE;
230
    }
231
232
    private static function keyEntrySize(bool $large): int
233
    {
234
        return $large ? self::KEY_ENTRY_SIZE_LARGE : self::KEY_ENTRY_SIZE_SMALL;
235
    }
236
237
    private static function valueEntrySize(bool $large): int
238
    {
239
        return $large ? self::VALUE_ENTRY_SIZE_LARGE : self::VALUE_ENTRY_SIZE_SMALL;
240
    }
241
242
    /**
243
     * @throws BinaryDataReaderException
244
     * @throws JsonBinaryDecoderException
245
     */
246
    private function getOffsetOrInLinedValue(int $bytes, int $intSize, int $valueEntrySize): JsonBinaryDecoderValue
247
    {
248
        $type = $this->binaryDataReader->readUInt8();
249
250
        if (self::isInLinedType($type, $intSize)) {
251
            $scalar = $this->parseScalar($type);
252
253
            // In binlog format, JSON arrays are fixed width elements, even though type value can be smaller.
254
            // In order to properly process this case, we need to move cursor to the next element, which is on position 1 + $valueEntrySize (1 is length of type)
255
            if($type === self::UINT16 || $type === self::INT16) {
256
                $readNextBytes = $valueEntrySize - 2 - 1;
257
                $this->binaryDataReader->read($readNextBytes);
258
            }
259
260
            return $scalar;
261
        }
262
263
        $offset = $this->binaryDataReader->readUIntBySize($intSize);
264
        if ($offset > $bytes) {
265
            throw new LengthException(
266
                '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)'
267
            );
268
        }
269
270
        return new JsonBinaryDecoderValue(false, null, $type, $offset);
271
    }
272
273
    private static function isInLinedType(int $type, int $intSize): bool
274
    {
275
        switch ($type) {
276
            case self::LITERAL:
277
            case self::INT16:
278
            case self::UINT16:
279
                return true;
280
            case self::INT32:
281
            case self::UINT32:
282
                return self::LARGE_OFFSET_SIZE === $intSize;
283
            default:
284
                return false;
285
        }
286
    }
287
288
    /**
289
     * @throws JsonBinaryDecoderException
290
     */
291
    private function parseScalar(int $type): JsonBinaryDecoderValue
292
    {
293
        if (self::LITERAL === $type) {
294
            $data = $this->readLiteral();
295
        } else if (self::INT16 === $type) {
296
            $data = $this->binaryDataReader->readInt16();
297
        } else if (self::INT32 === $type) {
298
            $data = ($this->binaryDataReader->readInt32());
299
        } else if (self::INT64 === $type) {
300
            $data = $this->binaryDataReader->readInt64();
301
        } else if (self::UINT16 === $type) {
302
            $data = ($this->binaryDataReader->readUInt16());
303
        } else if (self::UINT64 === $type) {
304
            $data = ($this->binaryDataReader->readUInt64());
305
        } else if (self::DOUBLE === $type) {
306
            $data = ($this->binaryDataReader->readDouble());
307
        } else if (self::STRING === $type) {
308
            $data = ($this->binaryDataReader->read($this->readVariableInt()));
309
        } /**
310
         * else if (self::OPAQUE === $type)
311
         * {
312
         *
313
         * }
314
         */
315
        else {
316
            throw new JsonBinaryDecoderException(
317
                JsonBinaryDecoderException::UNKNOWN_JSON_TYPE_MESSAGE . $type,
318
                JsonBinaryDecoderException::UNKNOWN_JSON_TYPE_CODE
319
            );
320
        }
321
322
        return new JsonBinaryDecoderValue(true, $data, $type);
323
    }
324
325
    private function readLiteral(): ?bool
326
    {
327
        $literal = ord($this->binaryDataReader->read(BinaryDataReader::UNSIGNED_SHORT_LENGTH));
328
        if (self::LITERAL_NULL === $literal) {
329
            return null;
330
        }
331
        if (self::LITERAL_TRUE === $literal) {
332
            return true;
333
        }
334
        if (self::LITERAL_FALSE === $literal) {
335
            return false;
336
        }
337
338
        return null;
339
    }
340
341
    private function readVariableInt(): int
342
    {
343
        $maxBytes = min($this->binaryDataReader->getBinaryDataLength(), 5);
344
        $len = 0;
345
        for ($i = 0; $i < $maxBytes; ++$i) {
346
            $size = $this->binaryDataReader->readUInt8();
347
            // Get the next 7 bits of the length.
348
            $len |= ($size & 0x7f) << (7 * $i);
349
            if (($size & 0x80) === 0) {
350
                // This was the last byte. Return successfully.
351
                return $len;
352
            }
353
        }
354
355
        return $len;
356
    }
357
358
    /**
359
     * @throws JsonBinaryDecoderException
360
     * @throws BinaryDataReaderException
361
     */
362
    private function assignValues(JsonBinaryDecoderValue $jsonBinaryDecoderValue): void
363
    {
364
        if (false === $jsonBinaryDecoderValue->isIsResolved()) {
365
            $this->ensureOffset($jsonBinaryDecoderValue->getOffset());
366
            $this->parseJson($jsonBinaryDecoderValue->getType());
367
        } else if (null === $jsonBinaryDecoderValue->getValue()) {
368
            $this->jsonBinaryDecoderFormatter->formatValueNull();
369
        } else if (is_bool($jsonBinaryDecoderValue->getValue())) {
370
            $this->jsonBinaryDecoderFormatter->formatValueBool($jsonBinaryDecoderValue->getValue());
371
        } else if (is_numeric($jsonBinaryDecoderValue->getValue())) {
372
            $this->jsonBinaryDecoderFormatter->formatValueNumeric($jsonBinaryDecoderValue->getValue());
373
        }
374
    }
375
376
    private function ensureOffset(?int $ensureOffset): void
377
    {
378
        if (null === $ensureOffset) {
379
            return;
380
        }
381
        $pos = $this->binaryDataReader->getReadBytes();
382
        if ($pos !== $ensureOffset) {
383
            if ($ensureOffset < $pos) {
384
                return;
385
            }
386
            $this->binaryDataReader->advance($ensureOffset + 1 - $pos);
387
        }
388
    }
389
}
390