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 */ |
|
|
|
|
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 */ |
|
|
|
|
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
|
|
|
} |
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.