JsonSafeRepair.php 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280
  1. <?php
  2. namespace App\Libs;
  3. /**
  4. * 剧本资产生成结果的 JSON 安全修复器
  5. *
  6. * 仅处理一种已验证可安全修复的语法错误:在数组(content 二维数组)中,
  7. * 一个元素已经结束后又额外出现了一个 "}"(多写的对象闭合符)。
  8. *
  9. * 修复采用逐字符解析(字符串内容完整跳过,不做任何修改),定位多余
  10. * 的 "}" 并删除;每次删除后都必须通过完整的 json_decode 校验才算成功,
  11. * 校验不通过则放弃修复并原样返回,绝不猜测补全其它符号。
  12. */
  13. class JsonSafeRepair
  14. {
  15. /**
  16. * 尝试安全修复 JSON。
  17. *
  18. * @param string $json 原始内容
  19. * @return array{ok:bool, json:?string, removed_positions:int[], reason:string}
  20. */
  21. public static function repair(string $json): array
  22. {
  23. $base = ['ok' => false, 'json' => null, 'removed_positions' => [], 'reason' => ''];
  24. if ($json === '') {
  25. $base['reason'] = 'empty';
  26. return $base;
  27. }
  28. // 本身就是合法 JSON,无需修复
  29. if (self::isValidJson($json)) {
  30. $base['ok'] = true;
  31. $base['json'] = $json;
  32. return $base;
  33. }
  34. $current = $json;
  35. $removed = [];
  36. for ($attempt = 0; $attempt < 30; $attempt++) {
  37. $index = self::findExtraClosingBrace($current);
  38. if ($index === null) {
  39. $base['reason'] = 'unsupported-syntax';
  40. $base['removed_positions'] = $removed;
  41. return $base;
  42. }
  43. $current = substr($current, 0, $index) . substr($current, $index + 1);
  44. $removed[] = $index;
  45. if (self::isValidJson($current)) {
  46. $base['ok'] = true;
  47. $base['json'] = $current;
  48. $base['removed_positions'] = $removed;
  49. return $base;
  50. }
  51. }
  52. $base['reason'] = 'too-many-attempts';
  53. $base['removed_positions'] = $removed;
  54. return $base;
  55. }
  56. private static function isValidJson(string $json): bool
  57. {
  58. json_decode($json, true);
  59. return json_last_error() === JSON_ERROR_NONE;
  60. }
  61. /**
  62. * 定位"数组内一个元素结束后多写了一个 }"的字符位置;无匹配返回 null。
  63. *
  64. * 实现说明:逐字符模拟 JSON 的括号/状态结构,字符串内容(含转义)完整跳过,
  65. * 不修改任何字符;仅当栈顶是数组、且该数组刚完成一个元素(期待 , 或 ])时
  66. * 遇到 "}",并且其后能自然续接(]、}、, 或结尾)才认为这是多余的 "}"。
  67. */
  68. private static function findExtraClosingBrace(string $json): ?int
  69. {
  70. $len = strlen($json);
  71. // 栈元素: ['t' => '['|'{', 'p' => 'value'|'key'|'colon'|'comma']
  72. $stack = [];
  73. $i = 0;
  74. while ($i < $len) {
  75. $ch = $json[$i];
  76. if ($ch === ' ' || $ch === "\t" || $ch === "\n" || $ch === "\r") {
  77. $i++;
  78. continue;
  79. }
  80. if ($ch === '"') {
  81. // 字符串:可能是对象的 key,也可能是 value
  82. if (!self::isStringStartAllowed($stack)) {
  83. return null;
  84. }
  85. $i = self::skipString($json, $i, $len);
  86. if ($i >= $len) {
  87. return null; // 字符串未闭合,不在安全修复范围
  88. }
  89. self::markValueDone($stack, true);
  90. $i++;
  91. continue;
  92. }
  93. if ($ch === '{' || $ch === '[') {
  94. if (!self::isValueStartAllowed($stack)) {
  95. return null;
  96. }
  97. $stack[] = ['t' => $ch, 'p' => $ch === '[' ? 'value' : 'key'];
  98. $i++;
  99. continue;
  100. }
  101. if ($ch === '}') {
  102. if (empty($stack)) {
  103. return null;
  104. }
  105. $topIndex = count($stack) - 1;
  106. $top = $stack[$topIndex];
  107. if ($top['t'] === '[') {
  108. if ($top['p'] === 'comma') {
  109. // 数组元素结束后出现多余的 }:仅当后面能自然续接时才删除
  110. $j = $i + 1;
  111. while ($j < $len && ($json[$j] === ' ' || $json[$j] === "\t" || $json[$j] === "\n" || $json[$j] === "\r")) {
  112. $j++;
  113. }
  114. if ($j >= $len || $json[$j] === ']' || $json[$j] === '}' || $json[$j] === ',') {
  115. return $i;
  116. }
  117. }
  118. return null;
  119. }
  120. // 对象闭合:空对象 {} 或 key:value 之后
  121. if ($top['p'] === 'key' || $top['p'] === 'comma') {
  122. array_pop($stack);
  123. self::markValueDone($stack, false);
  124. $i++;
  125. continue;
  126. }
  127. return null; // 冒号后缺值等,不在安全修复范围
  128. }
  129. if ($ch === ']') {
  130. if (empty($stack)) {
  131. return null;
  132. }
  133. $topIndex = count($stack) - 1;
  134. $top = $stack[$topIndex];
  135. if ($top['t'] === '[' && ($top['p'] === 'value' || $top['p'] === 'comma')) {
  136. array_pop($stack);
  137. self::markValueDone($stack, false);
  138. $i++;
  139. continue;
  140. }
  141. return null;
  142. }
  143. if ($ch === ',') {
  144. if (empty($stack)) {
  145. return null;
  146. }
  147. $topIndex = count($stack) - 1;
  148. if ($stack[$topIndex]['p'] === 'comma') {
  149. $stack[$topIndex]['p'] = $stack[$topIndex]['t'] === '[' ? 'value' : 'key';
  150. $i++;
  151. continue;
  152. }
  153. return null;
  154. }
  155. if ($ch === ':') {
  156. if (empty($stack)) {
  157. return null;
  158. }
  159. $topIndex = count($stack) - 1;
  160. if ($stack[$topIndex]['t'] === '{' && $stack[$topIndex]['p'] === 'colon') {
  161. $stack[$topIndex]['p'] = 'value';
  162. $i++;
  163. continue;
  164. }
  165. return null;
  166. }
  167. // 字面量 token(数字 / true / false / null)
  168. if (!self::isValueStartAllowed($stack)) {
  169. return null;
  170. }
  171. $i++;
  172. while ($i < $len) {
  173. $c = $json[$i];
  174. if ($c === ' ' || $c === "\t" || $c === "\n" || $c === "\r"
  175. || $c === ',' || $c === ']' || $c === '}' || $c === ':'
  176. || $c === '"' || $c === '{' || $c === '[') {
  177. break;
  178. }
  179. $i++;
  180. }
  181. self::markValueDone($stack, false);
  182. }
  183. // 完整扫描未发现可安全修复的多余 }
  184. return null;
  185. }
  186. /**
  187. * 跳过一段 JSON 字符串(含转义),返回闭合引号的位置;未闭合返回 $len。
  188. */
  189. private static function skipString(string $json, int $i, int $len): int
  190. {
  191. $i++;
  192. while ($i < $len) {
  193. $c = $json[$i];
  194. if ($c === '\\') {
  195. $i += 2;
  196. continue;
  197. }
  198. if ($c === '"') {
  199. return $i;
  200. }
  201. $i++;
  202. }
  203. return $len;
  204. }
  205. /**
  206. * 值(或对象的 key 字符串)是否允许在此位置开始。
  207. */
  208. private static function isStringStartAllowed(array $stack): bool
  209. {
  210. if (empty($stack)) {
  211. return true;
  212. }
  213. $top = end($stack);
  214. if ($top['t'] === '[') {
  215. return $top['p'] === 'value';
  216. }
  217. // 对象内:期待 key(字符串)或冒号后的 value
  218. return $top['p'] === 'key' || $top['p'] === 'value';
  219. }
  220. /**
  221. * 非字符串值(嵌套容器、数字、true/false/null)是否允许开始。
  222. */
  223. private static function isValueStartAllowed(array $stack): bool
  224. {
  225. if (empty($stack)) {
  226. return true;
  227. }
  228. $top = end($stack);
  229. if ($top['t'] === '[') {
  230. return $top['p'] === 'value';
  231. }
  232. // 对象内非字符串值只能是冒号后的 value
  233. return $top['p'] === 'value';
  234. }
  235. /**
  236. * 一个值(字符串/字面量/容器结束)之后,标记所在层进入"期待分隔符或闭合"状态。
  237. *
  238. * @param bool $stringToken 是否为字符串 token(字符串在对象 key 位置时是 key)
  239. */
  240. private static function markValueDone(array &$stack, bool $stringToken): void
  241. {
  242. if (empty($stack)) {
  243. return;
  244. }
  245. $topIndex = count($stack) - 1;
  246. if ($stringToken && $stack[$topIndex]['t'] === '{' && $stack[$topIndex]['p'] === 'key') {
  247. // key 字符串结束,期待冒号
  248. $stack[$topIndex]['p'] = 'colon';
  249. return;
  250. }
  251. $stack[$topIndex]['p'] = 'comma';
  252. }
  253. }