feat(collect): 按本次构建生成 collect 的工具链 + 回环回归测试
tools/tdc_collectgen.py: 静态导出槽位序 -> 按 #36 规则算「项->槽」-> 重排 -> 该构建密钥加密。 同一构建实测: 37 项 -> 38 槽, 明文 686B / collect 920 字符, 解密回环逐字节一致。 规则修正(重要): * kind=1 的串只按**括号外**逗号切槽 —— '[]'/'[360,360]'/'["zh-CN","zh"]' 内逗号不构成新槽 * kind=2 的项**仍产出 1 槽**(r3 清空转存 r4, r4 在收尾 @4905 拼回) 新增 tcaptcha/tests/test_collectgen.py (已并入 selftest) 与 fixture: outer_commas / 槽数推算 == 真机槽数 / 重排是置换 / 加解密回环 / 不匹配必须报错。 账本新增已知 #37(工具链跑通) 与 #38(纯离线生成尚未打通, 诚实标注)。
This commit is contained in:
@@ -16,6 +16,7 @@ SUITES = [
|
||||
"tcaptcha.tests.test_glyph_solver",
|
||||
"tcaptcha.tests.test_icon_click_solver",
|
||||
"tcaptcha.tests.test_slider_solver",
|
||||
"tcaptcha.tests.test_collectgen",
|
||||
"tcaptcha.tests.test_xtea_key",
|
||||
"tcaptcha.tests.test_tdc_key",
|
||||
]
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
{"seq": [8, 9, 10, 15, 18, 19, 20, 21, 27, 28, 31, 33, 34, 36, 37, 38, 39, 40, 41, 26, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 55, 1, 56, 57, 58], "kinds": [2, 1, null, null, null, null, null, null, 1, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, 1, null, null, null, null, null, 1, null, 1, null], "v0s": ["vXc29RxhvT34Qjg4BO81Rcg6kcS36LwS", "1", "1440", "0", "0", "0", "2560", "98k", "[]", "+08", "https://turing.captcha.gtimg.com/1/template/drag_ele.html?rand=1519713624347", "1440-2560-2530-24-*-*-|-*", "0", "MacIntel", "2", "https://1ms.run/console/personal/benefits/check-in", "1791736469", "", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/155.0.0.0 Safari/537.36", "1984571394", "ANGLE (Apple, ANGLE Metal Renderer: Apple M4, Unspecified Version)", "iframe", "", "0", "1023", "Google Inc. (Apple)", "", "[360,360]", "1791736491", "24", "UTF-8", "10", "GgoAAAANSUhEUgAAASwAAACWCAYAAATf3v8bh+ryWcfSBSLkmN3N1PQinPL7Bi3TG0qE2CogAAAABJRU5ErkJggg==", "641547489,1791732001", "2", "[\"zh-CN\",\"zh\"]", "0"], "sample_cd": [1791736322, 2560, [[1, 1, 12]], 0, "1440-2560-2530-24-*-*-|-*", 1440, "iframe", 0, "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/155.0.0.0 Safari/537.36", "ANGLE (Apple, ANGLE Metal Renderer: Apple M4, Unspecified Version)", "https://turing.captcha.gtimg.com/1/template/drag_ele.html?rand=1519713624347", "", 0, "+08", 1023, "", [360, 360], "UTF-8", "", 0, 2, 1791736260, 2086701610, 3, 1, "https://1ms.run/console/personal/benefits/check-in", "98k", [], "MacIntel", "Google Inc. (Apple)", "GgoAAAANSUhEUgAAASwAAACWCAYAAATf3v8bh+ryWcfSBSLkmN3N1PQinPL7Bi3TG0qE2CogAAAABJRU5ErkJggg==", 0, 10, 641547489, 1791732001, 24, ["zh-CN", "zh"], 0]}
|
||||
@@ -0,0 +1,105 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
按构建生成 collect 的回归测试(承 PROGRESS.md #34/#35/#36;AGENTS.md §5.9)。
|
||||
|
||||
固化的性质:
|
||||
1. `outer_commas`: 只数**括号外**的逗号 —— 方括号内的逗号不构成顶层分隔
|
||||
('[]' / '[360,360]' / '["zh-CN","zh"]' 拼回去仍是一个值);
|
||||
2. `SlotSpec` 推算出的槽数 == 真机明文 cd 的实际槽数;
|
||||
3. 重排是**置换**:值集合不变(排序后逐项相同);
|
||||
4. 加密/解密**回环一致**(明文逐字节还原);
|
||||
5. 槽数不匹配的样本**必须报错**,不能静默错位。
|
||||
|
||||
样本: tcaptcha/tests/fixtures/tdc_slotmap_case.json
|
||||
= {seq, kinds, v0s, sample_cd},取自 2026-10-11 真机会话(37 项 -> 38 槽)。
|
||||
|
||||
运行:
|
||||
.venv/bin/python -m tcaptcha.tests.test_collectgen
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
if str(ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(ROOT))
|
||||
if str(ROOT / "tools") not in sys.path:
|
||||
sys.path.insert(0, str(ROOT / "tools"))
|
||||
|
||||
from tcaptcha.crypto import xtea_decrypt_bytes # noqa: E402
|
||||
|
||||
import tdc_collectgen as cg # noqa: E402
|
||||
|
||||
FIX = ROOT / "tcaptcha" / "tests" / "fixtures" / "tdc_slotmap_case.json"
|
||||
# 固定密钥,仅用于回环自洽(不代表任何真实构建)
|
||||
KEY = [1497711441, 1213874789, 1799903316, 1835232858]
|
||||
|
||||
_fails: list = []
|
||||
|
||||
|
||||
def check(name: str, cond: bool, detail: str = "") -> None:
|
||||
print(f" [{'PASS' if cond else 'FAIL'}] {name}" + (f" {detail}" if detail else ""))
|
||||
if not cond:
|
||||
_fails.append(name)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
if not FIX.exists():
|
||||
print(f"缺少 fixture: {FIX}")
|
||||
return 1
|
||||
case = json.loads(FIX.read_text(encoding="utf-8"))
|
||||
seq, kinds, v0s, sample_cd = case["seq"], case["kinds"], case["v0s"], case["sample_cd"]
|
||||
print(f"fixture: 项 {len(seq)} / kind {len(kinds)} / v0 {len(v0s)} / 真机 cd {len(sample_cd)}")
|
||||
|
||||
print("\n[1] outer_commas: 只数括号外的逗号")
|
||||
check("'[]' -> 0", cg.outer_commas("[]") == 0)
|
||||
check("'[360,360]' -> 0", cg.outer_commas("[360,360]") == 0)
|
||||
check("'[\"zh-CN\",\"zh\"]' -> 0", cg.outer_commas('["zh-CN","zh"]') == 0)
|
||||
check("'641547489,1791732001' -> 1", cg.outer_commas("641547489,1791732001") == 1)
|
||||
check("'a,b,c' -> 2", cg.outer_commas("a,b,c") == 2)
|
||||
|
||||
print("\n[2] 槽数推算 == 真机明文槽数")
|
||||
spec = cg.SlotSpec(seq, kinds, v0s)
|
||||
print(f" {spec.describe()}")
|
||||
check("槽数一致", spec.n_slots == len(sample_cd),
|
||||
f"{spec.n_slots} vs {len(sample_cd)}")
|
||||
check("fixture 覆盖 kind=1", any(k == 1 for k in kinds))
|
||||
check("fixture 覆盖 kind=2", any(k == 2 for k in kinds))
|
||||
|
||||
print("\n[3] 重排是置换(值集合不变)")
|
||||
out = cg.reorder(sample_cd, spec)
|
||||
key = lambda v: json.dumps(v, sort_keys=True, ensure_ascii=False)
|
||||
check("长度不变", len(out) == len(sample_cd), f"{len(out)} vs {len(sample_cd)}")
|
||||
check("值集合相同(排序后逐项相等)",
|
||||
sorted(map(key, out)) == sorted(map(key, sample_cd)))
|
||||
|
||||
print("\n[4] 加密/解密回环")
|
||||
plain = cg.build_plain(out)
|
||||
b64 = cg.encrypt_plain(plain, KEY)
|
||||
back = xtea_decrypt_bytes(base64.b64decode(b64), KEY).decode("latin1").rstrip("\x00").strip()
|
||||
check("明文逐字节还原", back == plain)
|
||||
back_obj = json.loads(back)
|
||||
check("cd 还原", back_obj["cd"] == out)
|
||||
check("sd 还原", back_obj["sd"] == {"od": "C", "ft": "qf_7Pf__H"})
|
||||
|
||||
print("\n[5] 槽数不匹配必须报错")
|
||||
raised = False
|
||||
try:
|
||||
cg.reorder(sample_cd[:-1], spec)
|
||||
except ValueError:
|
||||
raised = True
|
||||
check("少一槽 -> ValueError", raised)
|
||||
|
||||
print(f"\n{'=' * 62}")
|
||||
if _fails:
|
||||
print(f"❌ collectgen 测试失败: {_fails}")
|
||||
return 1
|
||||
print("✅ collectgen 测试全部通过")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user