Passed
Push — master ( 4a06bc...a3462d )
by Moln
06:01 queued 02:08
created

JsonBinaryDecoderService::valueEntrySize()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 2

Importance

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