Completed
Branch master (e8656d)
by Delete
01:57
created

Decoder::decode()   B

Complexity

Conditions 3
Paths 4

Size

Total Lines 26
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 3
Bugs 0 Features 0
Metric Value
cc 3
eloc 13
c 3
b 0
f 0
nc 4
nop 4
dl 0
loc 26
rs 8.8571
1
<?php
2
namespace Crossjoin\Json;
3
4
use Crossjoin\Json\Exception\EncodingNotSupportedException;
5
use Crossjoin\Json\Exception\InvalidArgumentException;
6
use Crossjoin\Json\Exception\JsonException;
7
8
/**
9
 * Class Decoder
10
 *
11
 * @package Crossjoin\Json
12
 * @author Christoph Ziegenberg <[email protected]>
13
 */
14
class Decoder extends Converter
15
{
16
    /**
17
     * @var bool
18
     */
19
    private $ignoreByteOrderMark = true;
20
21
    /**
22
     * Decoder constructor.
23
     *
24
     * @param bool $ignoreByteOrderMark
25
     *
26
     * @throws \Crossjoin\Json\Exception\InvalidArgumentException
27
     */
28
    public function __construct($ignoreByteOrderMark = true)
29
    {
30
        $this->setIgnoreByteOrderMark($ignoreByteOrderMark);
31
    }
32
33
    /**
34
     * @return boolean
35
     */
36
    public function getIgnoreByteOrderMark()
37
    {
38
        return $this->ignoreByteOrderMark;
39
    }
40
41
    /**
42
     * @param boolean $ignoreByteOrderMark
43
     *
44
     * @throws \Crossjoin\Json\Exception\InvalidArgumentException
45
     */
46
    public function setIgnoreByteOrderMark($ignoreByteOrderMark)
47
    {
48
        // Check arguments
49
        InvalidArgumentException::validateArgument(InvalidArgumentException::TYPE_BOOLEAN, 'ignoreByteOrderMark', $ignoreByteOrderMark, 1478195542);
50
51
        $this->ignoreByteOrderMark = $ignoreByteOrderMark;
52
    }
53
54
    /**
55
     * Gets the encoding of the JSON text.
56
     *
57
     * @param string $json
58
     *
59
     * @return string
60
     * @throws \Crossjoin\Json\Exception\InvalidArgumentException
61
     * @throws \Crossjoin\Json\Exception\EncodingNotSupportedException
62
     */
63
    public function getEncoding($json)
64
    {
65
        // Check arguments
66
        InvalidArgumentException::validateArgument(InvalidArgumentException::TYPE_STRING, 'json', $json, 1478195652);
67
68
        // Get the first bytes
69
        $bytes = $this->getEncodingBytes($json);
70
71
        // Check encoding
72
        if (preg_match('/^(?:[^\x00]{1,3}$|[^\x00]{4})/', $bytes)) {
73
            // It's UTF-8 encoded JSON if you have...
74
            // - 1 byte and it's not NUL ("xx")
75
            // - 2 bytes and none of them are NUL ("xx xx")
76
            // - 3 bytes and they are not NUL ("xx xx xx")
77
            // - 4 or more bytes and the first 4 bytes are not NUL ("xx xx xx xx")
78
            //
79
            // BUT the check also matches UTF-8 ByteOrderMarks, which isn't allowed in JSON.
80
            // So we need to do an additional check (if ByteOrderMarks have not already been removed before)
81
            if ($this->ignoreByteOrderMark || !preg_match('/^\xEF\xBB\xBF/', $bytes)) {
82
                return self::UTF8;
83
            }
84
        } else if (preg_match('/^(?:\x00[^\x00]{1}$|\x00[^\x00]{1}.{2})/s', $bytes)) {
85
            // It's UTF-16BE encoded JSON if you have...
86
            // - 2 bytes and only the first is NUL ("00 xx")
87
            // - 4 or more bytes and only the first byte of the first 2 bytes is NUL ("00 xx")
88
            return self::UTF16BE;
89
        } else if (preg_match('/^(?:[^\x00]{1}\x00$|[^\x00]{1}\x00[^\x00]{1}.{1})/s', $bytes)) {
90
            // It's UTF-16LE encoded JSON if you have...
91
            // - 2 bytes and only the second is NUL ("xx 00")
92
            // - 4 or more bytes and only the second of the first 3 bytes is NUL ("xx 00 xx")
93
            return self::UTF16LE;
94
        } else if (preg_match('/^[\x00]{3}[^\x00]{1}/', $bytes)) {
95
            // It's UTF-32BE encoded JSON if you have...
96
            // - 4 or more bytes and only the first to third byte of the first 4 bytes are NUL ("00 00 00 xx")
97
            return self::UTF32BE;
98
        } else if (preg_match('/^[^\x00]{1}[\x00]{3}/', $bytes)) {
99
            // It's UTF-32LE encoded JSON if you have...
100
            // - 4 or more bytes and only the second to fourth byte of the first 4 bytes are NUL ("xx 00 00 00")
101
            return self::UTF32LE;
102
        }
103
104
        // No encoding found
105
        throw new EncodingNotSupportedException(
106
            'The JSON text is encoded with an unsupported encoding.',
107
            1478092834
108
        );
109
    }
110
111
    /** @noinspection MoreThanThreeArgumentsInspection */
112
    /**
113
     * Parses a valid JSON text that is encoded as UTF-8, UTF-16BE, UTF-16LE, UTF-32BE or UTF-32LE
114
     * and returns the data as UTF-8.
115
     *
116
     * @param string $json
117
     * @param bool $assoc
118
     * @param int $depth
119
     * @param int $options
120
     *
121
     * @return mixed
122
     * @throws \Crossjoin\Json\Exception\NativeJsonErrorException
123
     * @throws \Crossjoin\Json\Exception\ConversionFailedException
124
     * @throws \Crossjoin\Json\Exception\InvalidArgumentException
125
     * @throws \Crossjoin\Json\Exception\EncodingNotSupportedException
126
     * @throws \Crossjoin\Json\Exception\ExtensionRequiredException
127
     */
128
    public function decode($json, $assoc = false, $depth = 512, $options = 0)
129
    {
130
        // Check arguments
131
        InvalidArgumentException::validateArgument(InvalidArgumentException::TYPE_STRING, 'json', $json, 1478418105);
132
        InvalidArgumentException::validateArgument(InvalidArgumentException::TYPE_BOOLEAN, 'assoc', $assoc, 1478418106);
133
        InvalidArgumentException::validateArgument(InvalidArgumentException::TYPE_INTEGER, 'depth', $depth, 1478418107);
134
        InvalidArgumentException::validateArgument(InvalidArgumentException::TYPE_INTEGER, 'options', $options, 1478418108);
135
136
        // Prepare JSON data (remove BOMs and convert encoding)
137
        $json = $this->prepareJson($json);
138
139
        // Try to decode the json text
140
        // @codeCoverageIgnoreStart
141
        if (version_compare(PHP_VERSION, '5.4.0', '>=')) {
142
            $data = \json_decode($json, $assoc, $depth, $options);
143
        } else {
144
            $data = \json_decode($json, $assoc, $depth);
145
        }
146
        // @codeCoverageIgnoreEnd
147
148
        if (\json_last_error() !== \JSON_ERROR_NONE) {
149
            throw $this->getNativeJsonErrorException();
150
        }
151
152
        return $data;
153
    }
154
155
    /**
156
     * @param string $json
157
     *
158
     * @return string
159
     * @throws \Crossjoin\Json\Exception\InvalidArgumentException
160
     */
161
    private function getEncodingBytes($json)
162
    {
163
        // Do not use str_* function here because of possible mb_str_* overloading
164
        preg_match('/^(.{0,8})/s', $json, $matches);
165
        $bytes = array_key_exists(1, $matches) ? $matches[1] : '';
166
167
        // Remove byte order marks
168
        if ($this->ignoreByteOrderMark && $bytes !== '') {
169
            $bytes = $this->removeByteOrderMark($bytes);
170
        }
171
172
        return $bytes;
173
    }
174
175
    /**
176
     * @param string $json
177
     *
178
     * @return string
179
     */
180
    private function prepareJson($json)
181
    {
182
        try {
183
            // Ignore empty string
184
            // (will cause a parsing error in the native json_decode function)
185
            if ($json !== '') {
186
                // Remove byte order marks
187
                if ($this->ignoreByteOrderMark) {
188
                    $json = $this->removeByteOrderMark($json);
189
                }
190
191
                // Convert encoding to UTF-8
192
                $json = $this->convertEncoding($json, $this->getEncoding($json), self::UTF8);
193
            }
194
        } catch (JsonException $e) {
195
            // Ignore exception here, so that the native json_decode function
196
            // is called by the decode() method and we get the native error message.
197
        }
198
199
        return $json;
200
    }
201
}
202