Completed
Push — master ( e18fa7...d4c875 )
by Jitendra
10s
created

Fixer::quickFix()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 11
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 5
nc 3
nop 1
dl 0
loc 11
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace Ahc\Json;
4
5
/**
6
 * Attempts to fix truncated JSON by padding contextual counterparts at the end.
7
 *
8
 * @author  Jitendra Adhikari <[email protected]>
9
 * @license MIT
10
 *
11
 * @link    https://github.com/adhocore/php-json-fixer
12
 */
13
class Fixer
14
{
15
    use PadsJson;
16
17
    /** @var array Current token stack indexed by position */
18
    protected $stack = [];
19
20
    /** @var bool If current char is within a string */
21
    protected $inStr = false;
22
23
    /** @var bool Whether to throw Exception on failure */
24
    protected $silent = false;
25
26
    /** @var array The complementary pairs */
27
    protected $pairs = [
28
        '{' => '}',
29
        '[' => ']',
30
        '"' => '"',
31
    ];
32
33
    /** @var int The last seen object `{` type position */
34
    protected $objectPos = -1;
35
36
    /** @var int The last seen array `[` type position */
37
    protected $arrayPos  = -1;
38
39
    /** @var string Missing value. (Options: true, false, null) */
40
    protected $missingValue = 'null';
41
42
    /**
43
     * Set/unset silent mode.
44
     *
45
     * @param bool $silent
46
     *
47
     * @return $this
48
     */
49
    public function silent($silent = true)
50
    {
51
        $this->silent = (bool) $silent;
52
53
        return $this;
54
    }
55
56
    /**
57
     * Set missing value.
58
     *
59
     * @param mixed $value
60
     *
61
     * @return $this
62
     */
63
    public function missingValue($value)
64
    {
65
        if ($value === null) {
66
            $value = 'null';
67
        } elseif (\is_bool($value)) {
68
            $value = $value ? 'true' : 'false';
69
        }
70
71
        $this->missingValue = $value;
72
73
        return $this;
74
    }
75
76
    /**
77
     * Fix the truncated JSON.
78
     *
79
     * @param string $json The JSON string to fix.
80
     *
81
     * @throws \RuntimeExcaption When fixing fails.
82
     *
83
     * @return string Fixed JSON. If failed with silent then original JSON.
84
     */
85
    public function fix($json)
86
    {
87
        list($head, $json, $tail) = $this->trim($json);
88
89
        if (empty($json) || $this->isValid($json)) {
90
            return $json;
91
        }
92
93
        if (null !== $tmpJson = $this->quickFix($json)) {
94
            return $tmpJson;
95
        }
96
97
        $this->reset();
98
99
        return $head . $this->doFix($json) . $tail;
100
    }
101
102
    protected function trim($json)
103
    {
104
        \preg_match('/^(\s*)([^\s]+)(\s*)$/', $json, $match);
105
106
        $match += ['', '', '', ''];
107
        $match[2] = \trim($json);
108
109
        \array_shift($match);
110
111
        return $match;
112
    }
113
114
    protected function isValid($json)
115
    {
116
        \json_decode($json);
117
118
        return \JSON_ERROR_NONE === \json_last_error();
119
    }
120
121
    protected function quickFix($json)
122
    {
123
        if (\strlen($json) === 1 && isset($this->pairs[$json])) {
124
            return $json . $this->pairs[$json];
125
        }
126
127
        if ($json[0] !== '"') {
128
            return $this->maybeLiteral($json);
129
        }
130
131
        return $this->padString($json);
132
    }
133
134
    protected function reset()
135
    {
136
        $this->stack     = [];
137
        $this->inStr     = false;
138
        $this->objectPos = -1;
139
        $this->arrayPos  = -1;
140
    }
141
142
    protected function maybeLiteral($json)
143
    {
144
        if (!\in_array($json[0], ['t', 'f', 'n'])) {
145
            return null;
146
        }
147
148
        foreach (['true', 'false', 'null'] as $literal) {
149
            if (\strpos($literal, $json) === 0) {
150
                return $literal;
151
            }
152
        }
153
154
        // @codeCoverageIgnoreStart
155
        return null;
156
        // @codeCoverageIgnoreEnd
157
    }
158
159
    protected function doFix($json)
160
    {
161
        list($index, $char) = [-1, ''];
162
163
        while (isset($json[++$index])) {
164
            list($prev, $char) = [$char, $json[$index]];
165
166
            $next = isset($json[$index + 1]) ? $json[$index + 1] : '';
167
168
            if (!\in_array($char, [' ', "\n", "\r"])) {
169
                $this->stack($prev, $char, $index, $next);
170
            }
171
        }
172
173
        return $this->fixOrFail($json);
174
    }
175
176
    protected function stack($prev, $char, $index, $next)
0 ignored issues
show
Unused Code introduced by
The parameter $next is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

176
    protected function stack($prev, $char, $index, /** @scrutinizer ignore-unused */ $next)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
177
    {
178
        if ($this->maybeStr($prev, $char, $index)) {
179
            return;
180
        }
181
182
        $last = $this->lastToken();
183
184
        if (\in_array($last, [',', ':', '"']) && \preg_match('/\"|\d|\{|\[|t|f|n/', $char)) {
185
            $this->popToken();
186
        }
187
188
        if (\in_array($char, [',', ':', '[', '{'])) {
189
            $this->stack[$index] = $char;
190
        }
191
192
        $this->updatePos($char, $index);
193
    }
194
195
    protected function lastToken()
196
    {
197
        return \end($this->stack);
198
    }
199
200
    protected function popToken($token = null)
201
    {
202
        // Last one
203
        if (null === $token) {
204
            return \array_pop($this->stack);
205
        }
206
207
        $keys = \array_reverse(\array_keys($this->stack));
208
        foreach ($keys as $key) {
209
            if ($this->stack[$key] === $token) {
210
                unset($this->stack[$key]);
211
                break;
212
            }
213
        }
214
    }
215
216
    protected function maybeStr($prev, $char, $index)
217
    {
218
        if ($prev !== '\\' && $char === '"') {
219
            $this->inStr = !$this->inStr;
220
        }
221
222
        if ($this->inStr && $this->lastToken() !== '"') {
223
            $this->stack[$index] = '"';
224
        }
225
226
        return $this->inStr;
227
    }
228
229
    protected function updatePos($char, $index)
230
    {
231
        if ($char === '{') {
232
            $this->objectPos = $index;
233
        } elseif ($char === '}') {
234
            $this->popToken('{');
235
            $this->objectPos = -1;
236
        } elseif ($char === '[') {
237
            $this->arrayPos = $index;
238
        } elseif ($char === ']') {
239
            $this->popToken('[');
240
            $this->arrayPos = -1;
241
        }
242
    }
243
244
    protected function fixOrFail($json)
245
    {
246
        $length  = \strlen($json);
247
        $tmpJson = $this->pad($json);
248
249
        if ($this->isValid($tmpJson)) {
250
            return $tmpJson;
251
        }
252
253
        if ($this->silent) {
254
            return $json;
255
        }
256
257
        throw new \RuntimeException(
258
            \sprintf('Could not fix JSON (tried padding `%s`)', \substr($tmpJson, $length))
259
        );
260
    }
261
}
262