Passed
Pull Request — master (#53)
by kacper
04:21
created

JsonBinaryDecoderService::makeJsonBinaryDecoder()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
cc 1
eloc 3
c 0
b 0
f 0
nc 1
nop 1
dl 0
loc 5
ccs 0
cts 5
cp 0
crap 2
rs 10
1
<?php
2
declare(strict_types=1);
3
4
namespace MySQLReplication\JsonBinaryDecoder;
5
6
use LengthException;
7
use MySQLReplication\BinaryDataReader\BinaryDataReader;
8
use MySQLReplication\BinaryDataReader\BinaryDataReaderException;
9
10
/**
11
 * @see https://github.com/mysql/mysql-server/blob/5.7/sql/json_binary.cc
12
 * @see https://github.com/shyiko/mysql-binlog-connector-java/blob/master/src/main/java/com/github/shyiko/mysql/binlog/event/deserialization/json/JsonBinary.java
13
 */
14
class JsonBinaryDecoderService
15
{
16
    public const SMALL_OBJECT = 0;
17
    public const LARGE_OBJECT = 1;
18
    public const SMALL_ARRAY = 2;
19
    public const LARGE_ARRAY = 3;
20
    public const LITERAL = 4;
21
    public const INT16 = 5;
22
    public const UINT16 = 6;
23
    public const INT32 = 7;
24
    public const UINT32 = 8;
25
    public const INT64 = 9;
26
    public const UINT64 = 10;
27
    public const DOUBLE = 11;
28
    public const STRING = 12;
29
    public const OPAQUE = 15;
30
31
    private $binaryDataReader;
32
    private $jsonBinaryDecoderFormatter;
33
34
    public function __construct(
35
        BinaryDataReader $binaryDataReader,
36
        JsonBinaryDecoderFormatter $jsonBinaryDecoderFormatter
37
    ) {
38
        $this->binaryDataReader = $binaryDataReader;
39
        $this->jsonBinaryDecoderFormatter = $jsonBinaryDecoderFormatter;
40
    }
41
42
    public static function makeJsonBinaryDecoder(string $data): JsonBinaryDecoderService
43
    {
44
        return new JsonBinaryDecoderService(
45
            new BinaryDataReader($data),
46
            new JsonBinaryDecoderFormatter()
47
        );
48
    }
49
50
    /**
51
     * @throws BinaryDataReaderException
52
     * @throws JsonBinaryDecoderException
53
     */
54
    public function parseToString(): string
55
    {
56
        $this->parseJson($this->binaryDataReader->readUInt8());
57
58
        return $this->jsonBinaryDecoderFormatter->getJsonString();
59
    }
60
61
    /**
62
     * @throws JsonBinaryDecoderException
63
     * @throws BinaryDataReaderException
64
     */
65
    private function parseJson(int $type): void
66
    {
67
        if (self::SMALL_OBJECT === $type) {
68
            $this->parseObject(BinaryDataReader::UNSIGNED_SHORT_LENGTH);
69
        } else if (self::LARGE_OBJECT === $type) {
70
            $this->parseObject(BinaryDataReader::UNSIGNED_INT32_LENGTH);
71
        } else if (self::SMALL_ARRAY === $type) {
72
            $this->parseArray(BinaryDataReader::UNSIGNED_SHORT_LENGTH);
73
        } else if (self::LARGE_ARRAY === $type) {
74
            $this->parseObject(BinaryDataReader::UNSIGNED_INT32_LENGTH);
75
        } else {
76
            $this->parseScalar($type);
77
        }
78
    }
79
80
    /**
81
     * @throws BinaryDataReaderException
82
     * @throws JsonBinaryDecoderException
83
     */
84
    private function parseObject(int $intSize): void
85
    {
86
        $elementCount = $this->binaryDataReader->readUIntBySize($intSize);
87
        $size = $this->binaryDataReader->readUIntBySize($intSize);
88
89
        // Read each key-entry, consisting of the offset and length of each key ...
90
        $keyLengths = [];
91
        for ($i = 0; $i !== $elementCount; ++$i) {
92
            // $keyOffset unused
93
            $this->binaryDataReader->readUIntBySize($intSize);
94
            $keyLengths[$i] = $this->binaryDataReader->readUInt16();
95
        }
96
97
        $entries = [];
98
        for ($i = 0; $i !== $elementCount; ++$i) {
99
            $entries[$i] = $this->parseValueType($size, $intSize);
100
        }
101
102
        // Read each key ...
103
        $keys = [];
104
        for ($i = 0; $i !== $elementCount; ++$i) {
105
            $keys[$i] = $this->binaryDataReader->read($keyLengths[$i]);
106
        }
107
108
        $this->jsonBinaryDecoderFormatter->formatBeginObject();
109
110
        for ($i = 0; $i !== $elementCount; ++$i) {
111
            if ($i !== 0) {
112
                $this->jsonBinaryDecoderFormatter->formatNextEntry();
113
            }
114
115
            $this->jsonBinaryDecoderFormatter->formatName($keys[$i]);
116
117
            /* @var JsonBinaryDecoderValue[] $entries */
0 ignored issues
show
Unused Code Comprehensibility introduced by
56% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
118
            $this->assignValues($entries[$i]);
119
        }
120
121
        $this->jsonBinaryDecoderFormatter->formatEndObject();
122
    }
123
124
    /**
125
     * @throws BinaryDataReaderException
126
     */
127
    private function parseValueType(int $numBytes, int $intSize): JsonBinaryDecoderValue
128
    {
129
        $type = $this->binaryDataReader->readInt8();
130
131
        if (self::LITERAL === $type) {
132
            return new JsonBinaryDecoderValue(
133
                true,
134
                $this->readLiteral(),
135
                $type
136
            );
137
        }
138
139
        if (self::INT16 === $type) {
140
            return new JsonBinaryDecoderValue(
141
                true,
142
                $this->binaryDataReader->readInt16(),
143
                $type
144
            );
145
        }
146
147
        if (self::UINT16 === $type) {
148
            return new JsonBinaryDecoderValue(
149
                true,
150
                $this->binaryDataReader->readUInt16(),
151
                $type
152
            );
153
        }
154
155
        if (BinaryDataReader::UNSIGNED_INT32_LENGTH === $intSize) {
156
            if (self::INT32 === $type) {
157
                return new JsonBinaryDecoderValue(
158
                    true,
159
                    $this->binaryDataReader->readInt32(),
160
                    $type
161
                );
162
            }
163
164
            if (self::UINT32 === $type) {
165
                return new JsonBinaryDecoderValue(
166
                    true,
167
                    $this->binaryDataReader->readUInt32(),
168
                    $type
169
                );
170
            }
171
        }
172
173
        $offset = $this->binaryDataReader->readUIntBySize($intSize);
174
        if ($offset > $numBytes) {
175
            throw new LengthException(
176
                'The offset for the value in the JSON binary document is ' .
177
                $offset .
178
                ', which is larger than the binary form of the JSON document (' .
179
                $numBytes . ' bytes)'
180
            );
181
        }
182
183
        return new JsonBinaryDecoderValue(
184
            false,
185
            null,
186
            $type
187
        );
188
    }
189
190
    private function readLiteral(): ?bool
191
    {
192
        $literal = ord($this->binaryDataReader->read(BinaryDataReader::UNSIGNED_SHORT_LENGTH));
193
        if (0 === $literal) {
194
            return null;
195
        }
196
        if (1 === $literal) {
197
            return true;
198
        }
199
        if (2 === $literal) {
200
            return false;
201
        }
202
203
        return null;
204
    }
205
206
    /**
207
     * @throws JsonBinaryDecoderException
208
     * @throws BinaryDataReaderException
209
     */
210
    private function assignValues(JsonBinaryDecoderValue $jsonBinaryDecoderValue): void
211
    {
212
        if (false === $jsonBinaryDecoderValue->isIsResolved()) {
213
            $this->parseJson($jsonBinaryDecoderValue->getType());
214
        } else if (null === $jsonBinaryDecoderValue->getValue()) {
215
            $this->jsonBinaryDecoderFormatter->formatValueNull();
216
        } else if (is_bool($jsonBinaryDecoderValue->getValue())) {
217
            $this->jsonBinaryDecoderFormatter->formatValueBool($jsonBinaryDecoderValue->getValue());
218
        } else if (is_numeric($jsonBinaryDecoderValue->getValue())) {
219
            $this->jsonBinaryDecoderFormatter->formatValueNumeric($jsonBinaryDecoderValue->getValue());
220
        }
221
    }
222
223
    /**
224
     * @throws BinaryDataReaderException
225
     * @throws JsonBinaryDecoderException
226
     */
227
    private function parseArray(int $size): void
228
    {
229
        $numElements = $this->binaryDataReader->readUInt16();
230
        $numBytes = $this->binaryDataReader->readUInt16();
231
232
        $entries = [];
233
        for ($i = 0; $i !== $numElements; ++$i) {
234
            $entries[$i] = $this->parseValueType($numBytes, $size);
235
        }
236
237
        $this->jsonBinaryDecoderFormatter->formatBeginArray();
238
239
        for ($i = 0; $i !== $numElements; ++$i) {
240
            if ($i !== 0) {
241
                $this->jsonBinaryDecoderFormatter->formatNextEntry();
242
            }
243
244
            /* @var JsonBinaryDecoderValue[] $entries */
0 ignored issues
show
Unused Code Comprehensibility introduced by
56% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
245
            $this->assignValues($entries[$i]);
246
        }
247
248
        $this->jsonBinaryDecoderFormatter->formatEndArray();
249
    }
250
251
    /**
252
     * @throws JsonBinaryDecoderException
253
     */
254
    private function parseScalar(int $type): void
255
    {
256
        if (self::LITERAL === $type) {
257
            $this->parseBoolean();
258
        } else if (self::INT16 === $type) {
259
            $this->jsonBinaryDecoderFormatter->formatValue($this->binaryDataReader->readInt16());
260
        } else if (self::INT32 === $type) {
261
            $this->jsonBinaryDecoderFormatter->formatValue($this->binaryDataReader->readInt32());
262
        } else if (self::INT64 === $type) {
263
            $this->jsonBinaryDecoderFormatter->formatValue($this->binaryDataReader->readInt64());
264
        } else if (self::UINT16 === $type) {
265
            $this->jsonBinaryDecoderFormatter->formatValue($this->binaryDataReader->readUInt16());
266
        } else if (self::UINT64 === $type) {
267
            $this->jsonBinaryDecoderFormatter->formatValue($this->binaryDataReader->readUInt64());
268
        } else if (self::DOUBLE === $type) {
269
            $this->jsonBinaryDecoderFormatter->formatValue($this->binaryDataReader->readDouble());
270
        } else if (self::STRING === $type) {
271
            $this->jsonBinaryDecoderFormatter->formatValue($this->binaryDataReader->read($this->readVariableInt()));
272
        } /**
273
         * else if (self::OPAQUE === $type)
274
         * {
275
         *
276
         * }
277
         */
278
        else {
279
            throw new JsonBinaryDecoderException(
280
                JsonBinaryDecoderException::UNKNOWN_JSON_TYPE_MESSAGE . $type,
281
                JsonBinaryDecoderException::UNKNOWN_JSON_TYPE_CODE
282
            );
283
        }
284
    }
285
286
    private function parseBoolean(): void
287
    {
288
        $r = $this->readLiteral();
289
        if (null === $r) {
290
            $this->jsonBinaryDecoderFormatter->formatValue('null');
291
        } else {
292
            $this->jsonBinaryDecoderFormatter->formatValueBool($r);
293
        }
294
    }
295
296
    private function readVariableInt(): int
297
    {
298
        $length = $this->binaryDataReader->getBinaryDataLength();
299
        $len = 0;
300
        for ($i = 0; $i < $length; $i++) {
301
            $size = $this->binaryDataReader->readUInt8();
302
            // Get the next 7 bits of the length.
303
            $len |= ($size & 127) << (7 * $i);
304
            if (($size & 128) === 0) {
305
                // This was the last byte. Return successfully.
306
                return $len;
307
            }
308
        }
309
310
        return $len;
311
    }
312
}