Passed
Pull Request — master (#1)
by Jitendra
01:21
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 array The complementary pairs */
24
    protected $pairs = [
25
        '{' => '}',
26
        '[' => ']',
27
        '"' => '"',
28
    ];
29
30
    /** @var int The last seen object `{` type position */
31
    protected $objectPos = -1;
32
33
    /** @var int The last seen array `[` type position */
34
    protected $arrayPos  = -1;
35
36
    /** @var string Missing value. (Options: true, false, null) */
37
    protected $missingValue = 'true';
38
39
    /**
40
     * Fix the truncated JSON.
41
     *
42
     * @param string $json         The JSON string to fix.
43
     * @param bool   $silent       If silent, doesnt throw when fixing fails.
44
     * @param string $missingValue Missing value constructor. (Options: true, false, null).
45
     *
46
     * @throws \RuntimeExcaption When fixing fails.
47
     *
48
     * @return string Fixed JSON. If failed with silent then original JSON.
49
     */
50
    public function fix($json, $silent = false, $missingValue = 'null')
51
    {
52
        list($head, $json, $tail) = $this->trim($json);
53
54
        if (empty($json) || $this->isValid($json)) {
55
            return $json;
56
        }
57
58
        if (null !== $tmpJson = $this->quickFix($json)) {
59
            return $tmpJson;
60
        }
61
62
        $this->reset($missingValue);
63
64
        return $head . $this->doFix(\rtrim($json), $silent) . $tail;
65
    }
66
67
    public function trim($json)
68
    {
69
        \preg_match('/^(\s*)([^\s]+)(\s*)$/', $json, $match);
70
71
        $match += ['', '', \trim($json), ''];
72
73
        \array_shift($match);
74
75
        return $match;
76
    }
77
78
    protected function isValid($json)
79
    {
80
        \json_decode($json);
81
82
        return \JSON_ERROR_NONE === \json_last_error();
83
    }
84
85
    public function quickFix($json)
86
    {
87
        if (\strlen($json) === 1 && isset($this->pairs[$json])) {
88
            return $json . $this->pairs[$json];
89
        }
90
91
        if ($json[0] !== '"') {
92
            return $this->maybeLiteral($json);
93
        }
94
95
        return $this->padString($json);
96
    }
97
98
    protected function maybeLiteral($json)
99
    {
100
        if (!\in_array($json[0], ['t', 'f', 'n'])) {
101
            return null;
102
        }
103
104
        foreach (['true', 'false', 'null'] as $literal) {
105
            if (\strpos($literal, $json) === 0) {
106
                return $literal;
107
            }
108
        }
109
110
        // @codeCoverageIgnoreStart
111
        return null;
112
        // @codeCoverageIgnoreEnd
113
    }
114
115
    protected function reset($missingValue = 'null')
116
    {
117
        $this->stack = [];
118
        $this->inStr = false;
119
120
        $this->objectPos = -1;
121
        $this->arrayPos  = -1;
122
123
        $this->missingValue = $missingValue;
124
    }
125
126
    protected function doFix($json, $silent = false)
127
    {
128
        list($index, $char) = [-1, ''];
129
130
        while (isset($json[++$index])) {
131
            list($prev, $char) = [$char, $json[$index]];
132
133
            $next = isset($json[$index + 1]) ? $json[$index + 1] : '';
134
135
            if (!\in_array($char, [' ', "\n", "\r"])) {
136
                $this->stack($prev, $char, $index, $next);
137
            }
138
        }
139
140
        return $this->fixOrFail($json, $silent);
141
    }
142
143
    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

143
    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...
144
    {
145
        if ($this->maybeStr($prev, $char, $index)) {
146
            return;
147
        }
148
149
        $last = $this->lastToken();
150
151
        if (\in_array($last, [',', ':', '"']) && \preg_match('/\"|\d|\{|\[|t|f|n/', $char)) {
152
            $this->popToken();
153
        }
154
155
        if (\in_array($char, [',', ':', '[', '{'])) {
156
            $this->stack[$index] = $char;
157
        }
158
159
        $this->updatePos($char, $index);
160
    }
161
162
    protected function lastToken()
163
    {
164
        return \end($this->stack);
165
    }
166
167
    protected function popToken($token = null)
168
    {
169
        // Last one
170
        if (null === $token) {
171
            return \array_pop($this->stack);
172
        }
173
174
        $keys = \array_reverse(\array_keys($this->stack));
175
        foreach ($keys as $key) {
176
            if ($this->stack[$key] === $token) {
177
                unset($this->stack[$key]);
178
                break;
179
            }
180
        }
181
    }
182
183
    protected function maybeStr($prev, $char, $index)
184
    {
185
        if ($prev !== '\\' && $char === '"') {
186
            $this->inStr = !$this->inStr;
187
        }
188
189
        if ($this->inStr && $this->lastToken() !== '"') {
190
            $this->stack[$index] = '"';
191
        }
192
193
        return $this->inStr;
194
    }
195
196
    protected function updatePos($char, $index)
197
    {
198
        if ($char === '{') {
199
            $this->objectPos = $index;
200
        } elseif ($char === '}') {
201
            $this->popToken('{');
202
            $this->objectPos = -1;
203
        } elseif ($char === '[') {
204
            $this->arrayPos = $index;
205
        } elseif ($char === ']') {
206
            $this->popToken('[');
207
            $this->arrayPos = -1;
208
        }
209
    }
210
211
    protected function fixOrFail($json, $silent)
212
    {
213
        $length  = \strlen($json);
214
        $tmpJson = $this->pad($json);
215
216
        if ($this->isValid($tmpJson)) {
217
            return $tmpJson;
218
        }
219
220
        if ($silent) {
221
            return $json;
222
        }
223
224
        throw new \RuntimeException(
225
            \sprintf('Could not fix JSON (tried padding `%s`)', \substr($tmpJson, $length))
226
        );
227
    }
228
}
229