Files
hello_js_reverse_skill/tools/tdc_slotmap.py
T
yml2213 6460dd78d6 feat(VM): 打通「清单项 -> cd 槽」的精确映射规则, 新增 tools/tdc_slotmap.py
装配循环(@4362)分支树读通:
  kind===2(@4820) -> 不进 cd: 清空 r3 转存 r4, r4 在收尾 @4905 另作他用
  v===null/undefined(@4799) -> 'null'
  typeof v==='number'(@4738) -> (v||0)
  kind===1(@4724) -> 原样拼(v 是 JSON 碎片串, 串内逗号会切出额外槽)
  其余(@4703) -> 加引号
  收尾 @4753: typeof w==='number' 才再 +1 槽

同一构建端到端校验: 37 项 -> 38 槽, 仅第 33 项(模块1)占 2 槽
('641547489,1791732001' -> 641547489 | 1791732001 精确命中)。
工具支持 --items(运行时取样) + --plain(解密明文) 对拍。
2026-10-12 00:36:31 +08:00

175 lines
6.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
"""
tdc_slotmap.py —— 把「清单项」精确映射到「cd 槽号」。
为什么需要单独一层(承 har/1ms_dianxuan/PROGRESS.md #34/#35/#36):
#34 槽位序 = 清单模块的 require 顺序(追加式数组,已核)
#36 **槽数 ≠ 项数**: 装配循环里 `kind=1` 的分支是 `out += v`(不加引号直接拼),
所以某些项的返回值**本身带逗号**时会一次性产出多个槽。
实测同一构建: 37 项 -> 38 槽,多出的那一处是第 33 项(模块 1)
get() 返回 "641547489,1791732001"。
因此映射不能是"第 i 项 = 第 i 槽",必须按每个采集器的**返回值形状**推算:
kind = ret[2](可能缺省)
kind == 1: ret[0] 原样拼 -> 占 1 + ret[0].count(",") 槽(数字则 1 槽)
其它: ret[0] 占 1 槽
ret[1] 是 number: 再 +1 槽(实测 kind=2 的项第二元素是 null,不触发)
本模块提供:
* `slot_map(prog, items)` —— 给静态导出的项 + 运行时取样到的返回值形状,算出映射
* `from_runtime_items(prog)` —— 需要浏览器环境(iframe 里读清单各项 get()),见 CLI
* CLI 校验模式: 与一份已解密的 collect 明文对拍,确认推算出的槽数 == 实际槽数
用法:
# 用运行时取样 + 本次 getData 解密出的明文, 做一次端到端对拍
.venv/bin/python tools/tdc_slotmap.py <tdc.js> \\
--items /tmp/items.json --plain /tmp/plain.txt
"""
from __future__ import annotations
import argparse
import json
import sys
from pathlib import Path
from typing import Dict, List, Optional, Sequence, Tuple
ROOT = Path(__file__).resolve().parents[1]
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"))
import tdc_layout # noqa: E402
import tdc_program as tp # noqa: E402
def slots_of_item(ret) -> int:
"""一个采集器的 get() 返回值会占几个 cd 槽。
ret 是运行时取到的值(或其形状描述)。规则见 PROGRESS.md #36。
"""
if ret is None:
return 1
if not isinstance(ret, (list, tuple)) or not ret:
return 1
first = ret[0]
kind = ret[2] if len(ret) >= 3 else None
n = 1
if kind == 1 and isinstance(first, str):
# 不加引号直接拼 -> 串里的逗号会切出额外槽
n += first.count(",")
# 第二个元素是 number 时再占一槽(实测 kind=2 的第二元素是 null,不触发)
if len(ret) >= 2 and isinstance(ret[1], (int, float)) and not isinstance(ret[1], bool):
n += 1
return n
def slot_map(seq: Sequence[int], rets: Sequence) -> List[Tuple[int, int, int]]:
"""返回 [(清单项下标, 模块 id, 该首槽在 cd 中的槽号)]。
某一项占多槽时,只给它的**首槽**;其余槽是你按 ret 内容自然拼出来的。
"""
out: List[Tuple[int, int, int]] = []
slot = 0
for i, m in enumerate(seq):
out.append((i, m, slot))
slot += slots_of_item(rets[i] if i < len(rets) else None)
return out
def total_slots(seq: Sequence[int], rets: Sequence) -> int:
return sum(slots_of_item(rets[i] if i < len(rets) else None) for i in range(len(seq)))
def load_items(path: str) -> List:
"""读 cdp_eval 的取样输出({items:[{head,types,len,...}]})。
运行时只能带回 head(截断过),所以这里按 types/head 重建一个**够用的**形状。
"""
d = json.load(open(path, encoding="utf-8"))
res = d.get("result", d)
items = res.get("items") or []
rets = []
for it in items:
types = it.get("types")
head = it.get("head")
if not isinstance(types, list):
rets.append(None)
continue
rebuilt = []
for t, h in zip(types, head or []):
if t == "string":
rebuilt.append(h if isinstance(h, str) else str(h))
elif t == "number":
rebuilt.append(h if isinstance(h, (int, float)) else 0)
elif t == "null":
rebuilt.append(None)
elif t == "array":
rebuilt.append([])
else:
rebuilt.append(None)
rets.append(rebuilt)
return rets
def main() -> int:
ap = argparse.ArgumentParser(description="清单项 -> cd 槽号的精确映射")
ap.add_argument("path")
ap.add_argument("--items", default=None, help="运行时取样的 items.json")
ap.add_argument("--plain", default=None, help="已解密的 collect 明文(用于对拍校验)")
ap.add_argument("--json", default=None, help="把映射写成 JSON")
args = ap.parse_args()
prog = tp.load(args.path)
reg = dict(prog.module_registry(0))
_mid, _entry, seq = tdc_layout.find_manifest_module(prog)
rets: List = load_items(args.items) if args.items else []
print(f"文件 : {args.path}")
print(f"清单模块 : registry[{_mid}]")
print(f"清单项数 : {len(seq)}")
if args.items:
print(f"运行时取样 : {len(rets)} 项")
print(f"推算 cd 槽数 : {total_slots(seq, rets)}")
if rets:
print("\n项 模块 工厂 取到的值(截断) 占槽 首槽")
for i, m in enumerate(seq):
e = reg.get(m)
r = rets[i] if i < len(rets) else None
s = str(r)[:36]
n = slots_of_item(r)
start = slot_map(seq, rets)[i][2]
print(f"{i:2d} {m:3d} @{e:<6} {s:<38} {n:^3} {start:2d}")
if args.plain:
cd = json.loads(open(args.plain, encoding="latin1").read()
.rstrip("\x00").strip()).get("cd") or []
print(f"\n明文实际 cd 槽数: {len(cd)}")
if rets:
ok = len(cd) == total_slots(seq, rets)
print(f">>> 推算 == 实际 ? {ok}")
if not ok:
print(" (差 %d 槽: 检查是否有项返回值的逗号计数没算准)" % (len(cd) - total_slots(seq, rets)))
else:
print("\n逐项 -> 槽对位:")
sm = slot_map(seq, rets)
for (i, m, st) in sm:
n = slots_of_item(rets[i])
vals = [repr(cd[st + k])[:24] for k in range(min(n, len(cd) - st))]
print(f" 项{i:2d}(模块{m:2d}) -> 槽 {st}..{st + n - 1} {' | '.join(vals)}")
if args.json and rets:
Path(args.json).write_text(json.dumps(
{"manifestModule": _mid, "seq": list(seq),
"map": [{"item": i, "module": m, "firstSlot": st, "slots": slots_of_item(rets[i])}
for (i, m, st) in slot_map(seq, rets)]},
ensure_ascii=False, indent=1), encoding="utf-8")
print(f"映射 -> {args.json}")
return 0
if __name__ == "__main__":
raise SystemExit(main())