- codec_fields:module_87072::T5 记为单字段 bytes23;pq.tankState 的 TypeCodec 由 85183::jq 改指 87072::T5 - 生成器支持 bytes<N>(ByteArray / R.ReadRaw(N) / W.WriteRaw),重生成 Module87072、Module85183 - 审计链:lib_space_codec 新增 bytes<N>;audit_pt_boundaries / audit_bit_budget 支持 --body/--plain;semantic 暴露 Out.Bits - 新增工具:audit_bit_budget(nullmap 位预算)、probe_pt_record(pt 项级)、analyze_item_trace(客户端项级打点对照)、dump_codec_src - wss_server 新增 --dump-plain:把解密+解压后的裸帧体落到 plain/,供离线审计 - 镜像打点:opt 全量位位置、pt 项级字节/位区间、TankCC 逐字段区间;重新打包 unpacked.pack.json
154 lines
6.4 KiB
JavaScript
154 lines
6.4 KiB
JavaScript
// audit_bit_budget.js —— 用 nullmap 长度形态反推「真实 opt 位总数」,校验字段表的 opt 判定
|
||
//
|
||
// 原理(编码端选最长?不,选最短):nullmap 的长度形态由编码端按本次用到的位数选最短——
|
||
// 短形态(bit7=0):总长 1..4 字节,容量 5/13/21/29(扣掉 3 位长度标签)
|
||
// 长形态(bit7=1,bit6=0):1 字节标签 + L 字节,容量 8L
|
||
// 长形态(bit7=1,bit6=1):3 字节标签 + L 字节,容量 8L
|
||
// 所以「实际容量」必然等于 minimalCapacity(真实位数)。若和我们算出的位数不一致,
|
||
// 说明字段表在某些模型上少算/多算了 opt 位——这类错误**看不出来**:opt 位为 0(有值)时字节完全一样。
|
||
//
|
||
// 两种模式:
|
||
// --calib 只看「不含容器、无短命令」的帧:这些帧 semantic 的位游标就是我们的位数,直接比容量
|
||
// --pt 只看向量记录(pt)是最后一帧、且前面记录都能解:容器项解完后位游标应正好用满容量
|
||
//
|
||
// 用法:
|
||
// node audit_bit_budget.js --calib
|
||
// node audit_bit_budget.js --pt [--top 20]
|
||
'use strict';
|
||
const fs = require('fs');
|
||
const path = require('path');
|
||
const { LoadTables, MakeReader } = require('./lib_space_codec');
|
||
const Link = require('./server/link');
|
||
const Semantic = require('./server/semantic').Load(path.join(__dirname, '..', 'names'));
|
||
|
||
const PROJECT = path.resolve(__dirname, '..');
|
||
const Argv = process.argv.slice(2);
|
||
const Arg = (N, D) => { const I = Argv.indexOf('--' + N); return I >= 0 && Argv[I + 1] ? Argv[I + 1] : D; };
|
||
const Mode = Argv.indexOf('--pt') >= 0 ? 'pt' : 'calib';
|
||
const Top = Number(Arg('top', '20')) || 20;
|
||
// --body:文件即帧体(无长度头),配合 wss_server.js --dump-plain 的产物
|
||
const BodyMode = Argv.indexOf('--body') >= 0;
|
||
const PT_METHOD = '7640916300855664666';
|
||
|
||
function CapacityOf(Count) {
|
||
if (Count <= 29) return 8 * Math.ceil((Count + 3) / 8) - 3;
|
||
if (Count <= 504) return 8 * (Math.ceil((Count + 8) / 8) - 1);
|
||
return 8 * Math.ceil(Count / 8);
|
||
}
|
||
|
||
const Tables = LoadTables(path.join(PROJECT, 'names'));
|
||
const Binding = JSON.parse(fs.readFileSync(path.join(PROJECT, 'names', 'model_binding.json'), 'utf8'));
|
||
const CodecKeyByModelId = new Map();
|
||
for (const E of Binding.Entries) CodecKeyByModelId.set(String(E.ModelId), E.Key);
|
||
const RESOURCE_CLASS_WIRE = {
|
||
Ea: 'i64', nh: 'i64', W_: 'i64', uo: 'i64', pv: 'i64', B_: 'i64', Da: 'i64',
|
||
qs: 'i64', uh: 'i64', Gf: 'i64', kv: 'i64', Pc: 'i64',
|
||
};
|
||
const Alias = {};
|
||
for (const [N, R] of Object.entries(Tables.ResourceClasses || {})) Alias[N] = R.Wire;
|
||
for (const [N, W] of Object.entries(RESOURCE_CLASS_WIRE)) Alias[N] = W;
|
||
const Reader = MakeReader(Tables, {
|
||
EnumBytes: 4, EnumInList: 4, OptBits: true, TypeAlias: Alias, StringLiteralOnly: true,
|
||
});
|
||
|
||
function NullmapBits(Bytes) {
|
||
const Bits = [];
|
||
const Push = (B) => { for (let K = 7; K >= 0; K--) Bits.push((B >> K) & 1); };
|
||
if (Bytes.length && Bytes[0] & 0x80) {
|
||
for (let I = Bytes[0] & 0x40 ? 3 : 1; I < Bytes.length; I++) Push(Bytes[I]);
|
||
} else {
|
||
for (const B of Bytes) Push(B);
|
||
Bits.splice(0, 3);
|
||
}
|
||
return Bits;
|
||
}
|
||
|
||
const Dir = path.resolve(Arg('plain', path.join(PROJECT, 'packets', 'plain')));
|
||
const Files = [];
|
||
function Walk(P) {
|
||
for (const E of fs.readdirSync(P, { withFileTypes: true })) {
|
||
const Q = path.join(P, E.name);
|
||
if (E.isDirectory()) Walk(Q);
|
||
else if (/^s2c_\d+\.bin$/.test(E.name)) Files.push(Q);
|
||
}
|
||
}
|
||
Walk(Dir);
|
||
|
||
let Checked = 0;
|
||
const Delta = new Map();
|
||
const Rows = [];
|
||
for (const File of Files) {
|
||
// --body:文件即帧体(无长度头)
|
||
const Bodies = [];
|
||
if (BodyMode) {
|
||
try { Bodies.push(fs.readFileSync(File)); } catch (E) { continue; }
|
||
} else {
|
||
let Parsed;
|
||
try { Parsed = Link.ParseFrames(fs.readFileSync(File)); } catch (E) { continue; }
|
||
for (const F of Parsed || []) {
|
||
try { Bodies.push(Link.DecodeFrame(F)); } catch (E) {}
|
||
}
|
||
}
|
||
for (const Body of Bodies) {
|
||
let Dec;
|
||
try { Dec = Semantic.DecodeFrameBody(Body); } catch (E) { continue; }
|
||
const Recs = Dec.Records || [];
|
||
if (!Recs.length) continue;
|
||
const NmSize = Link.NullmapSize(Body, 0);
|
||
if (NmSize <= 0 || NmSize > Body.length) continue;
|
||
const Pb = Body[0];
|
||
const ActualCap = Pb & 0x80 ? 8 * (NmSize - (Pb & 0x40 ? 3 : 1)) : 8 * NmSize - 3;
|
||
|
||
let Our = null;
|
||
let Tail = '';
|
||
if (Mode === 'calib') {
|
||
// 纯字段表帧:所有记录都不走容器、不是短命令
|
||
if (Dec.Bad) continue;
|
||
if (Recs.some((R) => R.Layout || R.Short || R.Bad)) continue;
|
||
if (typeof Dec.Bits !== 'number') continue;
|
||
Our = Dec.Bits;
|
||
Tail = String(Recs[Recs.length - 1].MethodId);
|
||
} else {
|
||
// 容器帧:最后一条记录是 pt,前面没有解不动的
|
||
const R = Recs[Recs.length - 1];
|
||
if (String(R.MethodId) !== PT_METHOD || R.At === undefined) continue;
|
||
if (Recs.slice(0, -1).some((X) => X.Bad)) continue;
|
||
const Bits = NullmapBits(Body.slice(0, NmSize));
|
||
Reader.AttachBits(Bits, R.BitStart + 1);
|
||
let At = R.At + 16;
|
||
let Bad = false;
|
||
try {
|
||
const C = Body.readInt32BE(At);
|
||
At += 4 + 16 * C;
|
||
const N = Body.readInt32BE(At);
|
||
At += 4;
|
||
for (let I = 0; I < N; I++) {
|
||
const Id = Body.readBigInt64BE(At).toString();
|
||
At += 8;
|
||
if (Id === '0') { At += 8; continue; }
|
||
const Key = CodecKeyByModelId.get(Id) || '未注册';
|
||
Tail = Key;
|
||
const R2 = Reader.ReadFields(Body, At, Tables.CodecByKey.get(Key), 0);
|
||
At = R2.Next;
|
||
if (R2.Bad) { Bad = true; break; }
|
||
}
|
||
} catch (E) { Bad = true; }
|
||
Our = Reader.BitCursor().Index;
|
||
Reader.DetachBits();
|
||
if (Bad) continue;
|
||
}
|
||
Checked++;
|
||
const Want = CapacityOf(Our);
|
||
Delta.set(ActualCap - Want, (Delta.get(ActualCap - Want) || 0) + 1);
|
||
if (Want !== ActualCap) Rows.push({ File: path.relative(Dir, File), Our, ActualCap, Want, Tail });
|
||
}
|
||
}
|
||
console.log('[' + Mode + '] 可核对帧 ' + Checked + ',容量差分布(实际 - 期望):' +
|
||
JSON.stringify([...Delta.entries()].sort((A, B) => B[1] - A[1])));
|
||
Rows.sort((A, B) => (B.ActualCap - B.Our) - (A.ActualCap - A.Our));
|
||
console.log('不匹配 ' + Rows.length + ' 帧:');
|
||
for (const R of Rows.slice(0, Top)) {
|
||
console.log(' ' + R.File + ' 我们 ' + R.Our + ' 位(期望容量 ' + R.Want + ') 实际容量 ' + R.ActualCap +
|
||
' 差 ' + (R.ActualCap - R.Our) + ' 末项 ' + R.Tail);
|
||
}
|