JsonBinaryDecoderService   F
last analyzed

Complexity

Total Complexity 75

Size/Duplication

Total Lines 365
Duplicated Lines 0 %

Test Coverage

Coverage 0%

Importance

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