Passed
Pull Request — master (#1)
by Jitendra
01:25
created

Fixer::fixOrFail()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 15
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 8
nc 3
nop 2
dl 0
loc 15
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')
0 ignored issues
show
Unused Code introduced by
The parameter $silent 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

50
    public function fix($json, /** @scrutinizer ignore-unused */ $silent = false, $missingValue = 'null')

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...
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)) . $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 (\is_numeric($json)) {
88
            return $json;
89
        }
90
91
        if (\strlen($json) === 1 && isset($this->pairs[$json])) {
92
            return $json . $this->pairs[$json];
93
        }
94
95
        if ($json[0] !== '"') {
96
            return $this->maybeLiteral($json);
97
        }
98
99
        $last  = \substr($json, -1);
100
        $last2 = \substr($json, -2);
101
102
        if ($last2 === '\"' || $last !== '"') {
103
            return $json . '"';
104
        }
105
106
        return null;
107
    }
108
109
    protected function maybeLiteral($json)
110
    {
111
        if (!\in_array($json[0], ['t', 'f', 'n'])) {
112
            return null;
113
        }
114
115
        foreach (['true', 'false', 'null'] as $literal) {
116
            if (\strpos($literal, $json) === 0) {
117
                return $literal;
118
            }
119
        }
120
121
        return null;
122
    }
123
124
    protected function reset($missingValue = 'null')
125
    {
126
        $this->stack = [];
127
        $this->inStr = false;
128
129
        $this->objectPos = -1;
130
        $this->arrayPos  = -1;
131
132
        $this->missingValue = $missingValue;
133
    }
134
135
    protected function doFix(string $json, $silent = false)
136
    {
137
        list($index, $char) = [-1, ''];
138
139
        while (isset($json[++$index])) {
140
            list($prev, $char) = [$char, $json[$index]];
141
142
            $next = isset($json[$index + 1]) ? $json[$index + 1] : '';
143
144
            if (!\in_array($char, [' ', "\n", "\r"])) {
145
                $this->stack($prev, $char, $index, $next);
146
            }
147
        }
148
149
        return $this->fixOrFail($json, $silent);
150
    }
151
152
    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

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