| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280 |
- <?php
- namespace App\Libs;
- /**
- * 剧本资产生成结果的 JSON 安全修复器
- *
- * 仅处理一种已验证可安全修复的语法错误:在数组(content 二维数组)中,
- * 一个元素已经结束后又额外出现了一个 "}"(多写的对象闭合符)。
- *
- * 修复采用逐字符解析(字符串内容完整跳过,不做任何修改),定位多余
- * 的 "}" 并删除;每次删除后都必须通过完整的 json_decode 校验才算成功,
- * 校验不通过则放弃修复并原样返回,绝不猜测补全其它符号。
- */
- class JsonSafeRepair
- {
- /**
- * 尝试安全修复 JSON。
- *
- * @param string $json 原始内容
- * @return array{ok:bool, json:?string, removed_positions:int[], reason:string}
- */
- public static function repair(string $json): array
- {
- $base = ['ok' => false, 'json' => null, 'removed_positions' => [], 'reason' => ''];
- if ($json === '') {
- $base['reason'] = 'empty';
- return $base;
- }
- // 本身就是合法 JSON,无需修复
- if (self::isValidJson($json)) {
- $base['ok'] = true;
- $base['json'] = $json;
- return $base;
- }
- $current = $json;
- $removed = [];
- for ($attempt = 0; $attempt < 30; $attempt++) {
- $index = self::findExtraClosingBrace($current);
- if ($index === null) {
- $base['reason'] = 'unsupported-syntax';
- $base['removed_positions'] = $removed;
- return $base;
- }
- $current = substr($current, 0, $index) . substr($current, $index + 1);
- $removed[] = $index;
- if (self::isValidJson($current)) {
- $base['ok'] = true;
- $base['json'] = $current;
- $base['removed_positions'] = $removed;
- return $base;
- }
- }
- $base['reason'] = 'too-many-attempts';
- $base['removed_positions'] = $removed;
- return $base;
- }
- private static function isValidJson(string $json): bool
- {
- json_decode($json, true);
- return json_last_error() === JSON_ERROR_NONE;
- }
- /**
- * 定位"数组内一个元素结束后多写了一个 }"的字符位置;无匹配返回 null。
- *
- * 实现说明:逐字符模拟 JSON 的括号/状态结构,字符串内容(含转义)完整跳过,
- * 不修改任何字符;仅当栈顶是数组、且该数组刚完成一个元素(期待 , 或 ])时
- * 遇到 "}",并且其后能自然续接(]、}、, 或结尾)才认为这是多余的 "}"。
- */
- private static function findExtraClosingBrace(string $json): ?int
- {
- $len = strlen($json);
- // 栈元素: ['t' => '['|'{', 'p' => 'value'|'key'|'colon'|'comma']
- $stack = [];
- $i = 0;
- while ($i < $len) {
- $ch = $json[$i];
- if ($ch === ' ' || $ch === "\t" || $ch === "\n" || $ch === "\r") {
- $i++;
- continue;
- }
- if ($ch === '"') {
- // 字符串:可能是对象的 key,也可能是 value
- if (!self::isStringStartAllowed($stack)) {
- return null;
- }
- $i = self::skipString($json, $i, $len);
- if ($i >= $len) {
- return null; // 字符串未闭合,不在安全修复范围
- }
- self::markValueDone($stack, true);
- $i++;
- continue;
- }
- if ($ch === '{' || $ch === '[') {
- if (!self::isValueStartAllowed($stack)) {
- return null;
- }
- $stack[] = ['t' => $ch, 'p' => $ch === '[' ? 'value' : 'key'];
- $i++;
- continue;
- }
- if ($ch === '}') {
- if (empty($stack)) {
- return null;
- }
- $topIndex = count($stack) - 1;
- $top = $stack[$topIndex];
- if ($top['t'] === '[') {
- if ($top['p'] === 'comma') {
- // 数组元素结束后出现多余的 }:仅当后面能自然续接时才删除
- $j = $i + 1;
- while ($j < $len && ($json[$j] === ' ' || $json[$j] === "\t" || $json[$j] === "\n" || $json[$j] === "\r")) {
- $j++;
- }
- if ($j >= $len || $json[$j] === ']' || $json[$j] === '}' || $json[$j] === ',') {
- return $i;
- }
- }
- return null;
- }
- // 对象闭合:空对象 {} 或 key:value 之后
- if ($top['p'] === 'key' || $top['p'] === 'comma') {
- array_pop($stack);
- self::markValueDone($stack, false);
- $i++;
- continue;
- }
- return null; // 冒号后缺值等,不在安全修复范围
- }
- if ($ch === ']') {
- if (empty($stack)) {
- return null;
- }
- $topIndex = count($stack) - 1;
- $top = $stack[$topIndex];
- if ($top['t'] === '[' && ($top['p'] === 'value' || $top['p'] === 'comma')) {
- array_pop($stack);
- self::markValueDone($stack, false);
- $i++;
- continue;
- }
- return null;
- }
- if ($ch === ',') {
- if (empty($stack)) {
- return null;
- }
- $topIndex = count($stack) - 1;
- if ($stack[$topIndex]['p'] === 'comma') {
- $stack[$topIndex]['p'] = $stack[$topIndex]['t'] === '[' ? 'value' : 'key';
- $i++;
- continue;
- }
- return null;
- }
- if ($ch === ':') {
- if (empty($stack)) {
- return null;
- }
- $topIndex = count($stack) - 1;
- if ($stack[$topIndex]['t'] === '{' && $stack[$topIndex]['p'] === 'colon') {
- $stack[$topIndex]['p'] = 'value';
- $i++;
- continue;
- }
- return null;
- }
- // 字面量 token(数字 / true / false / null)
- if (!self::isValueStartAllowed($stack)) {
- return null;
- }
- $i++;
- while ($i < $len) {
- $c = $json[$i];
- if ($c === ' ' || $c === "\t" || $c === "\n" || $c === "\r"
- || $c === ',' || $c === ']' || $c === '}' || $c === ':'
- || $c === '"' || $c === '{' || $c === '[') {
- break;
- }
- $i++;
- }
- self::markValueDone($stack, false);
- }
- // 完整扫描未发现可安全修复的多余 }
- return null;
- }
- /**
- * 跳过一段 JSON 字符串(含转义),返回闭合引号的位置;未闭合返回 $len。
- */
- private static function skipString(string $json, int $i, int $len): int
- {
- $i++;
- while ($i < $len) {
- $c = $json[$i];
- if ($c === '\\') {
- $i += 2;
- continue;
- }
- if ($c === '"') {
- return $i;
- }
- $i++;
- }
- return $len;
- }
- /**
- * 值(或对象的 key 字符串)是否允许在此位置开始。
- */
- private static function isStringStartAllowed(array $stack): bool
- {
- if (empty($stack)) {
- return true;
- }
- $top = end($stack);
- if ($top['t'] === '[') {
- return $top['p'] === 'value';
- }
- // 对象内:期待 key(字符串)或冒号后的 value
- return $top['p'] === 'key' || $top['p'] === 'value';
- }
- /**
- * 非字符串值(嵌套容器、数字、true/false/null)是否允许开始。
- */
- private static function isValueStartAllowed(array $stack): bool
- {
- if (empty($stack)) {
- return true;
- }
- $top = end($stack);
- if ($top['t'] === '[') {
- return $top['p'] === 'value';
- }
- // 对象内非字符串值只能是冒号后的 value
- return $top['p'] === 'value';
- }
- /**
- * 一个值(字符串/字面量/容器结束)之后,标记所在层进入"期待分隔符或闭合"状态。
- *
- * @param bool $stringToken 是否为字符串 token(字符串在对象 key 位置时是 key)
- */
- private static function markValueDone(array &$stack, bool $stringToken): void
- {
- if (empty($stack)) {
- return;
- }
- $topIndex = count($stack) - 1;
- if ($stringToken && $stack[$topIndex]['t'] === '{' && $stack[$topIndex]['p'] === 'key') {
- // key 字符串结束,期待冒号
- $stack[$topIndex]['p'] = 'colon';
- return;
- }
- $stack[$topIndex]['p'] = 'comma';
- }
- }
|