Files
TermiNexus 90c377ad79 feat(3dtank): 本地镜像与 hornet 工具链迁到新版 bundle main.6469b966.js
旧 main.b7114b43.js 已被新构建替换(10,011,039 字节 / 1335 模块),本地镜像整体重切到新版并清掉旧产物;hornet 工具链按 tools/map_symbols.js 生成的新旧符号映射改名后重新打包(3dtank_bundle.js 与 unpacked/hornet_bundle.js 同步)。tools/symbol_map.json(8.9MB 映射产物)与切割解包目录不进库,可用 map_symbols.js / split.js 重新生成。
2026-09-17 19:59:40 +08:00

431 lines
14 KiB
JavaScript
Raw Permalink 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.
// map_symbols.js - 生成新旧客户端 bundle 之间的符号映射表
// 原理:两版源码除"混淆重命名"外高度一致。用 K 元 token 块做锚点对齐,
// 对齐后区域若 token 数相同则按位置一一对应,据此对标识符改名投票。
// 用法: node map_symbols.js <旧unpacked目录> <新bundle路径> <模块id逗号分隔> [输出json]
// 输出: JSON,结构 { 模块id: { 旧名: 新名 }, "_merged": { 旧名: 新名 } }
const fs = require('fs');
const path = require('path');
const K = 10; // 锚点块长度(token 数)
const MIN_ID_LEN = 3; // 参与投票的标识符最小长度(过滤局部单字符名)
// ===== 词法切分:标识符 / 数字 / 字符串 / 正则 / 标点 =====
function Tokenize(src) {
const out = [];
let i = 0;
const n = src.length;
const IdStart = /[A-Za-z_$]/;
const IdPart = /[A-Za-z0-9_$]/;
while (i < n) {
const c = src[i];
// 空白(换行/空格/制表)不参与比对
if (c === ' ' || c === '\n' || c === '\r' || c === '\t' || c === '\f' || c === '\v') { i++; continue; }
// 注释
if (c === '/' && src[i + 1] === '/') {
while (i < n && src[i] !== '\n') i++;
continue;
}
if (c === '/' && src[i + 1] === '*') {
i += 2;
while (i < n && !(src[i] === '*' && src[i + 1] === '/')) i++;
i += 2;
continue;
}
// 字符串
if (c === '"' || c === "'" || c === '`') {
const quote = c;
let j = i + 1;
while (j < n) {
if (src[j] === '\\') { j += 2; continue; }
if (src[j] === quote) { j++; break; }
j++;
}
out.push({ k: 'str', s: src.slice(i, j) });
i = j;
continue;
}
// 正则字面量(启发式:前一个有效字符不是标识符/数字/右括号)
if (c === '/') {
let p = out.length - 1;
const prev = p >= 0 ? out[p] : null;
const okPrev = !prev || (prev.k === 'punct' && !')]'.includes(prev.s));
if (okPrev) {
let j = i + 1;
let closed = false;
while (j < n) {
if (src[j] === '\\') { j += 2; continue; }
if (src[j] === '\n') break;
if (src[j] === '[') { while (j < n && src[j] !== ']') { if (src[j] === '\\') j++; j++; } }
if (src[j] === '/') { j++; closed = true; break; }
j++;
}
if (closed) {
while (j < n && /[a-z]/.test(src[j])) j++;
out.push({ k: 're', s: src.slice(i, j) });
i = j;
continue;
}
}
}
if (IdStart.test(c)) {
let j = i + 1;
while (j < n && IdPart.test(src[j])) j++;
out.push({ k: 'id', s: src.slice(i, j) });
i = j;
continue;
}
if (/[0-9]/.test(c)) {
let j = i + 1;
while (j < n && /[0-9a-fA-FxX._]/.test(src[j])) j++;
out.push({ k: 'num', s: src.slice(i, j) });
i = j;
continue;
}
out.push({ k: 'punct', s: c });
i++;
}
return out;
}
// ===== 从新 bundle 提取模块源码 =====
function ExtractNewModule(bundle, id) {
const re = /(?<=[{,])(\d{3,6}):(?:\(|function)/g;
let m;
const marks = [];
while ((m = re.exec(bundle)) !== null) {
marks.push({ id: m[1], start: m.index, keyEnd: m.index + m[1].length });
}
for (let i = 0; i < marks.length; i++) {
if (marks[i].id !== String(Number(id))) continue;
const end = i + 1 < marks.length ? marks[i + 1].start : bundle.length;
// 从 ':' 之后开始(跳过 "87072:")
return bundle.slice(marks[i].keyEnd + 1, end);
}
return null;
}
// ===== 读旧模块源码(去掉文件头注释)=====
function ReadOldModule(dir, id) {
const f = path.join(dir, 'module_' + id + '.js');
if (!fs.existsSync(f)) return null;
let src = fs.readFileSync(f, 'utf8');
src = src.replace(/^(?:\s*\/\/[^\n]*\n)+/, '');
return src;
}
// ===== 过滤"无信息"标点 =====
// 两版由不同代 minifier 产出:括号/逗号/分号的增删不影响语义,但会打断对齐,故丢弃。
// 保留:标识符/数字/字符串/正则,以及 {} . [] = + - * / % < > ! & | ? : ~ ^
const SIG_PUNCT = '{}.[]=+-*/%<>!&|?:~^';
function FilterSignificant(tokens) {
const out = [];
for (const t of tokens) {
if (t.k === 'punct') {
if (SIG_PUNCT.indexOf(t.s) < 0) continue;
out.push(t);
} else {
out.push(t);
}
}
return out;
}
// ===== 流式对齐 =====
// 两个 token 流同源,差异只有两类:①标识符改名 ②minifier 风格差异/少量源码改动。
// 策略:同步推进;标识符对标识符即记录一条改名投票;否则在有限窗口内找一个"4 token 完全一致块"重同步。
const LOOK = 40; // 重同步搜索窗口(token 数)
const SYNC_LEN = 4; // 重同步锚块长度
function SameTok(a, b) {
return a.k === b.k && a.s === b.s;
}
function SameBlock(oldT, i, newT, j, len) {
if (i + len > oldT.length || j + len > newT.length) return false;
for (let x = 0; x < len; x++) if (!SameTok(oldT[i + x], newT[j + x])) return false;
return true;
}
function WalkAlign(oldT, newT, onIdPair, counter, onUnsync, look) {
const LOOK2 = look || LOOK;
let i = 0, j = 0;
const n = oldT.length, m = newT.length;
while (i < n && j < m) {
const a = oldT[i], b = newT[j];
if (SameTok(a, b)) { i++; j++; counter.same++; continue; }
if (a.k === 'id' && b.k === 'id') { onIdPair(a.s, b.s); i++; j++; counter.rename++; continue; }
// 重同步:优先以字符串/数字常量定位,其次 4-token 块
let bestK = -1, bestL = -1;
for (let k = 0; k <= LOOK2 && i + k < n; k++) {
const ta = oldT[i + k];
if (ta.k !== 'str' && ta.k !== 'num') continue;
for (let l = 0; l <= LOOK2 && j + l < m; l++) {
const tb = newT[j + l];
if (SameTok(ta, tb)) { bestK = k; bestL = l; break; }
}
if (bestK >= 0) break;
}
if (bestK < 0) {
for (let k = 0; k <= LOOK2 && i + k < n; k++) {
for (let l = 0; l <= LOOK2 && j + l < m; l++) {
if (SameBlock(oldT, i + k, newT, j + l, SYNC_LEN)) { bestK = k; bestL = l; break; }
}
if (bestK >= 0) break;
}
}
if (bestK < 0) {
if (onUnsync) onUnsync(i, j, oldT, newT);
i++; j++; counter.unsync++; continue;
}
counter.sync += (bestK + bestL);
i += bestK;
j += bestL;
}
}
// ===== 递归唯一 token 切分 =====
// 思路(patience diff):在两侧都"只出现一次"的 token 天然对应,可作锚点切分;
// 切出的段内再递归找唯一 token,直到段足够小,再做局部对齐。
const REGION_MAX = 4000; // 单段最大 token 数,超过则继续切分
const REGION_BAD = 0.25; // 段内未同步比例上限,超过则丢弃该段的投票
const MAX_DEPTH = 12;
function LowerBound(arr, v) {
let lo = 0, hi = arr.length;
while (lo < hi) {
const mid = (lo + hi) >> 1;
if (arr[mid] < v) lo = mid + 1; else hi = mid;
}
return lo;
}
function UniqueAnchors(oldT, newT) {
const cntO = new Map(), posO = new Map();
for (let i = 0; i < oldT.length; i++) {
const k = oldT[i].k + ':' + oldT[i].s;
cntO.set(k, (cntO.get(k) || 0) + 1);
posO.set(k, i);
}
const cntN = new Map(), posN = new Map();
for (let j = 0; j < newT.length; j++) {
const k = newT[j].k + ':' + newT[j].s;
cntN.set(k, (cntN.get(k) || 0) + 1);
posN.set(k, j);
}
const pairs = [];
for (const [k, c] of cntO) {
if (c !== 1 || cntN.get(k) !== 1) continue;
pairs.push([posO.get(k), posN.get(k)]);
}
pairs.sort((a, b) => a[0] - b[0]);
return LisLongest(pairs);
}
// 取按第二坐标严格递增的最长子序列,保证锚点单调
function LisLongest(pairs) {
const tails = [];
const prev = new Array(pairs.length).fill(-1);
for (let i = 0; i < pairs.length; i++) {
const v = pairs[i][1];
let lo = 0, hi = tails.length;
while (lo < hi) {
const mid = (lo + hi) >> 1;
if (pairs[tails[mid]][1] < v) lo = mid + 1; else hi = mid;
}
prev[i] = lo > 0 ? tails[lo - 1] : -1;
tails[lo] = i;
}
const out = [];
let k = tails.length ? tails[tails.length - 1] : -1;
while (k >= 0) { out.push(pairs[k]); k = prev[k]; }
return out.reverse();
}
// 段内局部对齐(带质量门限,低质段整段丢弃)
function LocalWalk(oldT, os, oe, newT, ns, ne, sink, counter) {
const oLen = oe - os, nLen = ne - ns;
if (oLen <= 0 || nLen <= 0) return;
if (oLen > REGION_MAX || nLen > REGION_MAX) { counter.skipBig++; return; }
const pairs = [];
const c = { same: 0, rename: 0, sync: 0, unsync: 0 };
WalkAlign(oldT.slice(os, oe), newT.slice(ns, ne), (a, b) => pairs.push([a, b]), c, null, 150);
counter.same += c.same;
counter.rename += c.rename;
counter.sync += c.sync;
counter.unsync += c.unsync;
const bad = (c.unsync * 2) / (oLen + nLen || 1);
if (bad > REGION_BAD) { counter.badRegion++; return; }
for (const [a, b] of pairs) sink(a, b);
}
function RecursiveAlign(oldT, newT, sink, counter, depth) {
const anchors = UniqueAnchors(oldT, newT);
if (anchors.length === 0) {
LocalWalk(oldT, 0, oldT.length, newT, 0, newT.length, sink, counter);
return;
}
const parts = [];
let oi = 0, nj = 0;
for (const [i, j] of anchors) {
parts.push([oi, i, nj, j]);
oi = i + 1;
nj = j + 1;
}
parts.push([oi, oldT.length, nj, newT.length]);
for (const [os, oe, ns, ne] of parts) {
const oLen = oe - os, nLen = ne - ns;
if (oLen <= 0 || nLen <= 0) continue;
if (oLen <= REGION_MAX && nLen <= REGION_MAX) {
LocalWalk(oldT, os, oe, newT, ns, ne, sink, counter);
} else if (depth < MAX_DEPTH) {
RecursiveAlign(oldT.slice(os, oe), newT.slice(ns, ne), sink, counter, depth + 1);
} else {
counter.skipBig++;
}
}
}
function AlignByAnchors(oldT, newT, sink, counter) {
RecursiveAlign(oldT, newT, sink, counter, 0);
}
// ===== 主流程 =====
function SumVotes(votes) {
const merged = {};
const detail = {};
for (const [oldName, m] of votes) {
let best = null, total = 0;
for (const [k, v] of m) {
total += v;
if (!best || v > best.v) best = { k, v };
}
merged[oldName] = best.k;
detail[oldName] = {
to: best.k,
keep: best.k === oldName,
votes: best.v,
total,
ratio: +(best.v / total).toFixed(3),
};
}
return { merged, detail };
}
function BuildMap(oldDir, newBundlePath, ids) {
const bundle = fs.readFileSync(newBundlePath, 'utf8');
const perModule = {};
const globalVotes = new Map();
const stats = [];
for (const id of ids) {
const oldSrc = ReadOldModule(oldDir, id);
const newSrc = ExtractNewModule(bundle, id);
if (!oldSrc || !newSrc) {
stats.push({ id, error: 'missing source' });
continue;
}
const oldT = FilterSignificant(Tokenize(oldSrc));
const newT = FilterSignificant(Tokenize(newSrc));
const counter = { same: 0, rename: 0, sync: 0, unsync: 0, skipBig: 0, badRegion: 0, voted: 0 };
const votes = new Map();
AlignByAnchors(oldT, newT, (a, b) => {
if (a.length < MIN_ID_LEN) return;
counter.voted++;
if (!votes.has(a)) votes.set(a, new Map());
const m = votes.get(a);
m.set(b, (m.get(b) || 0) + 1);
if (!globalVotes.has(a)) globalVotes.set(a, new Map());
const g = globalVotes.get(a);
g.set(b, (g.get(b) || 0) + 1);
}, counter);
const per = {};
for (const [oldName, m] of votes) {
let best = null, total = 0;
for (const [k, v] of m) {
total += v;
if (!best || v > best.v) best = { k, v };
}
if (best.v >= 2 && best.v / total >= 0.6) per[oldName] = best.k;
}
perModule[id] = per;
stats.push({
id,
oldTok: oldT.length,
newTok: newT.length,
same: counter.same,
rename: counter.rename,
sync: counter.sync,
unsync: counter.unsync,
skipBig: counter.skipBig,
badRegion: counter.badRegion,
voted: counter.voted,
mapped: Object.keys(per).length,
});
}
const { merged, detail } = SumVotes(globalVotes);
perModule['_merged'] = merged;
perModule['_detail'] = detail;
perModule['_stats'] = stats;
// 跨模块冲突检测:同一旧名在不同模块映射到不同新名
const conflicts = [];
const seen = new Map();
for (const id of ids) {
const per = perModule[id];
if (!per) continue;
for (const [oldName, newName] of Object.entries(per)) {
if (!seen.has(oldName)) { seen.set(oldName, new Map()); }
const m = seen.get(oldName);
if (!m.has(newName)) m.set(newName, []);
m.get(newName).push(id);
}
}
for (const [oldName, m] of seen) {
if (m.size > 1) {
const items = [];
for (const [newName, ids2] of m) items.push(newName + '(' + ids2.join('/') + ')');
conflicts.push(oldName + ' => ' + items.join(' | '));
}
}
perModule['_conflicts'] = conflicts;
return perModule;
}
function main() {
const oldDir = process.argv[2];
const newBundle = process.argv[3];
let ids = (process.argv[4] || '').split(',').filter(Boolean);
const outPath = process.argv[5] || path.join(__dirname, 'symbol_map.json');
if (!oldDir || !newBundle || ids.length === 0) {
console.error('用法: node map_symbols.js <旧unpacked目录> <新bundle> <模块id逗号分隔|all> [输出json]');
process.exit(1);
}
if (ids.length === 1 && ids[0] === 'all') {
ids = fs.readdirSync(oldDir)
.filter((f) => /^module_\d+\.js$/.test(f))
.map((f) => f.replace(/^module_|\.js$/g, ''));
console.log('模块数: ' + ids.length);
}
const result = BuildMap(oldDir, newBundle, ids);
fs.writeFileSync(outPath, JSON.stringify(result, null, 2), 'utf8');
console.log('=== 统计 ===');
for (const s of result._stats) {
console.log(s.error
? ('[' + s.id + '] ' + s.error)
: ('[' + s.id + '] 旧token=' + s.oldTok + ' 新token=' + s.newTok +
' 一致=' + s.same + ' 改名=' + s.rename + ' 重同步丢失=' + s.sync + ' 未同步=' + s.unsync +
' 跳过段=' + s.skipBig + ' 低质段=' + s.badRegion + ' 投票=' + s.voted + ' 映射=' + s.mapped));
}
const names = Object.keys(result._merged);
console.log('=== 映射条目: ' + names.length + ' ===');
console.log('输出: ' + outPath);
}
module.exports = { Tokenize, FilterSignificant, ExtractNewModule, ReadOldModule, WalkAlign, UniqueAnchors, RecursiveAlign, AlignByAnchors, BuildMap };
if (require.main === module) main();