刘光辉
昨天 bb638871a7fb692d80f1b7a758f991dc0879002c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
package jnpf.bizcommon.audit.service;
 
import com.alibaba.fastjson.JSONException;
import jnpf.util.JsonUtil;
 
import java.lang.reflect.Method;
import java.util.Map;
 
/**
 * 层 0 负缓存的**失败分类**探针(C1 二轮评审 I-3 的证据脚本)。
 *
 * <p>为什么必须实测而不能推:{@code AuditFormRegistry.deterministicParseFailure} 决定
 * 「这次解析失败要不要进负缓存」。判错的代价不对称——把**瞬时**失败判成确定性,该表单
 * 从此静默不被审计直到重启(缓存是进程级、无过期的);反过来只是多查一次库。
 * L061 ① 记的就是这个坑,而它当时正是**推**出来的黑名单被实证推翻的。
 *
 * <p>本探针喂各种畸形输入给 {@code JsonUtil.getJsonToList}(层 0 反查主表用的正是它),
 * 打印真实异常链,再让 {@code deterministicParseFailure} 对它做判定,逐条断言。
 *
 * <p>退出码:0 = 全绿 / 1 = 有断言失败。**不连库、不起容器**,秒级。
 */
public final class AuditL0ParseFailureProbe {
 
    private static int passed;
    private static int failed;
 
    public static void main(String[] args) throws Exception {
        Method decide = AuditFormRegistry.class
                .getDeclaredMethod("deterministicParseFailure", Throwable.class);
        decide.setAccessible(true);
 
        System.out.println("=== 层0 解析失败分类探针(fastjson 实测链形 + 白名单判定)===");
 
        // ① 各类语法错误:应抛 JSONException → 确定性 → 可缓存
        expect(decide, "截断的 JSON", "[{\"table\":\"lims_x\"", true);
        expect(decide, "顶层非数组", "{\"table\":\"lims_x\"}", true);
        expect(decide, "元素非对象", "[1,2,3]", true);
        expect(decide, "空串以外的纯垃圾", "not json at all", true);
 
        // ② 畸形 Unicode 转义:**实测抛裸的 NumberFormatException、不被包装成 JSONException**
        //    (codex 独立复核发现,本探针独立复核其发现)。它同样是文本的固有属性 → 确定性。
        expect(decide, "畸形 Unicode 转义 \\uZZZZ", "[{\"table\":\"\\uZZZZ\"}]", true);
 
        // ③ 瞬时故障:**绝不能**判成确定性
        assertDecide(decide, "StackOverflowError(深层嵌套的真实形态)",
                new StackOverflowError(), false);
        assertDecide(decide, "OutOfMemoryError", new OutOfMemoryError(), false);
        assertDecide(decide, "线程中断", new InterruptedException(), false);
        assertDecide(decide, "包了一层的 SOE(池线程形态)",
                new RuntimeException(new StackOverflowError()), false);
 
        // ④ 自环 cause 链不能把遍历卡死(L061 的写法要求)。
        //    注意**不能**用 `e.initCause(e)` 构造——JDK 直接抛 IllegalArgumentException:
        //    Self-causation not permitted(本探针第一版就这么写的,当场炸了)。
        //    真实的自环只可能来自覆盖了 getCause() 的自定义异常,所以照那个形态造。
        assertDecide(decide, "自环 cause 链不死循环(覆盖 getCause 的形态)",
                new SelfCausedException(), false);
 
        System.out.println();
        System.out.printf("=== 层0 失败分类断言:%d 通过 / %d 失败 ===%n", passed, failed);
        if (failed > 0) {
            System.exit(1);
        }
    }
 
    /** 真喂一段畸形 JSON,打印它实际抛的异常链,再断言白名单的判定。 */
    private static void expect(Method decide, String label, String json, boolean deterministic) {
        Throwable caught = null;
        try {
            JsonUtil.getJsonToList(json, Map.class);
        } catch (Throwable t) {
            caught = t;
        }
        if (caught == null) {
            // 没抛异常也是有效信息(说明 fastjson 宽容地接受了它),不算失败,但要打印出来
            System.out.printf("  [INFO] %s → 未抛异常(fastjson 接受了这段输入)%n", label);
            return;
        }
        System.out.printf("  链形 %-28s → %s%n", label, chain(caught));
        assertDecide(decide, label, caught, deterministic);
    }
 
    private static void assertDecide(Method decide, String label, Throwable t, boolean expected) {
        boolean actual;
        try {
            actual = (Boolean) decide.invoke(null, t);
        } catch (Exception e) {
            System.out.printf("  [FAIL] %s → 判定方法自身抛异常:%s%n", label, e);
            failed++;
            return;
        }
        if (actual == expected) {
            System.out.printf("  [PASS] %s → %s%n", label,
                    expected ? "确定性(可负缓存)" : "瞬时(不缓存)");
            passed++;
        } else {
            System.out.printf("  [FAIL] %s → 判为%s,应为%s%s%n", label,
                    actual ? "确定性" : "瞬时", expected ? "确定性" : "瞬时",
                    expected ? "" : "  ← 危险方向:瞬时失败被永久负缓存 = 该表单静默不再被审计");
            failed++;
        }
    }
 
    private static String chain(Throwable t) {
        StringBuilder sb = new StringBuilder();
        for (Throwable c = t; c != null; c = (c.getCause() == c ? null : c.getCause())) {
            if (sb.length() > 0) {
                sb.append(" ← ");
            }
            sb.append(c.getClass().getSimpleName());
            if (c instanceof JSONException || c instanceof NumberFormatException) {
                sb.append("*");   // * = 命中白名单的那一环
            }
        }
        return sb.toString();
    }
 
    /** 唯一能造出自环 cause 的形态:覆盖 getCause() 返回自己。 */
    private static final class SelfCausedException extends RuntimeException {
        @Override
        public synchronized Throwable getCause() {
            return this;
        }
    }
 
    private AuditL0ParseFailureProbe() {
    }
}