|
|
@@ -0,0 +1,280 @@
|
|
|
+<?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';
|
|
|
+ }
|
|
|
+}
|