fix(tools): 审计改用跨记录位游标,修正多记录帧误判
This commit is contained in:
1 parent
8444b961c0
commit
ce8eeec8d6
5 files changed
+67
-29
No files matched your search
@@ -16,6 +16,7 @@ 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 Argv = process.argv.slice(2);
|
||||
const Arg = (Name, Def) => {
|
||||
@@ -50,8 +51,14 @@ const TraceArg = Arg('trace', '');
|
||||
const TraceDepth = Number(Arg('trace-depth', '6')) || 6;
|
||||
let TraceOn = false;
|
||||
|
||||
const EnumBytesArg = Arg('enum-bytes', '4');
|
||||
const EnumBytes = EnumBytesArg === undefined || EnumBytesArg === '' ? 4 : Number(EnumBytesArg);
|
||||
const EnumInListArg = Arg('enum-in-list', '');
|
||||
const EnumInList = EnumInListArg === '' ? EnumBytes : Number(EnumInListArg);
|
||||
if (!Number.isFinite(EnumBytes) || !Number.isFinite(EnumInList)) throw new Error('--enum-bytes/--enum-in-list 需要数字');
|
||||
const Reader = MakeReader(Tables, {
|
||||
EnumBytes: Number(Arg('enum-bytes', '4')) || 4,
|
||||
EnumBytes,
|
||||
EnumInList,
|
||||
OptBits: true,
|
||||
TypeAlias: Alias,
|
||||
StringLiteralOnly: true,
|
||||
@@ -127,7 +134,7 @@ function IsBoundary(Buf, At) {
|
||||
return false;
|
||||
}
|
||||
|
||||
function AuditPt(Body, Off, File) {
|
||||
function AuditPt(Body, Off, File, BitStart) {
|
||||
if (Off + 4 > Body.length) return;
|
||||
const Buf = Body;
|
||||
const C = Buf.readInt32BE(Off);
|
||||
@@ -138,11 +145,13 @@ function AuditPt(Body, Off, File) {
|
||||
At += 4;
|
||||
if (D < 0 || D > 1 << 20) return;
|
||||
Stats.Records++;
|
||||
// opt 字段的「空/非空」位与记录形态位共用同一条 nullmap 比特流;本审计只处理「pt 是帧内第一条记录」
|
||||
// 的情形,所以位游标 = 1(跳过本条记录的形态位),随后由 Reader 内部逐个 opt 推进。
|
||||
// opt 字段的「空/非空」位与记录形态位共用同一条 nullmap 比特流;
|
||||
// 起始位索引由 semantic.DecodeFrameBody 顺序推进得出(见下面的 BitStart)。
|
||||
const NmSize = Link.NullmapSize(Body, 0);
|
||||
const Bits = NmSize > 0 && NmSize <= Body.length ? NullmapBits(Body.slice(0, NmSize)) : null;
|
||||
if (Bits) Reader.AttachBits(Bits, 1);
|
||||
// 位游标由 semantic 的顺序推进给出(BitStart = 本记录形态位之后的绝对位索引),
|
||||
// 不再假设「pt 是帧内第一条记录」——多记录帧里那样会整体错位。
|
||||
if (Bits) Reader.AttachBits(Bits, typeof BitStart === 'number' ? BitStart : 1);
|
||||
try {
|
||||
for (let I = 0; I < D; I++) {
|
||||
if (At + 8 > Buf.length) return;
|
||||
@@ -213,14 +222,17 @@ function ScanFile(File) {
|
||||
} catch (E) {
|
||||
continue;
|
||||
}
|
||||
const Start = Link.NullmapSize(Body, 0);
|
||||
let Off = Start + 8;
|
||||
while (Off + 8 <= Body.length) {
|
||||
const MethodId = Body.readBigInt64BE(Off).toString();
|
||||
// 只认 pt;其它记录的负载长度这里不建模,遇到就结束本帧
|
||||
if (MethodId !== PT_METHOD) break;
|
||||
AuditPt(Body, Off + 8, path.basename(File));
|
||||
break;
|
||||
// 用 semantic 顺序解完整帧:它维护跨记录的位游标,并给每条记录标出 BitStart。
|
||||
// 只审计 pt 记录;其它记录的负载长度由 semantic 负责推进(短命令会中断整帧,属已知限制)。
|
||||
let Decoded = null;
|
||||
try {
|
||||
Decoded = Semantic.DecodeFrameBody(Body);
|
||||
} catch (E) {
|
||||
continue;
|
||||
}
|
||||
for (const R of (Decoded && Decoded.Records) || []) {
|
||||
if (String(R.MethodId) !== PT_METHOD || R.Bad || R.At === undefined || R.Consumed === undefined) continue;
|
||||
AuditPt(Body, R.At + 16, path.basename(File), R.BitStart);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,7 +108,14 @@ function SplitTypeArgs(Text) {
|
||||
|
||||
function MakeReader(Tables, Options) {
|
||||
const Opts = Options || {};
|
||||
const EnumBytes = Opts.EnumBytes === undefined ? 4 : Opts.EnumBytes;
|
||||
// enum 宽度:源码上 EnumCodecInfo(vn → Kt)读写都是空实现(0 字节),但实测数据自相矛盾——
|
||||
// EmissionColorCC 的 4×i32 正好到帧尾(该 enum 必须 0 字节),而 enum=0 又会让 P_/Tu 等 21 个模型错位。
|
||||
// 全量审计下 4 字节口径最优(6 处未命中 vs 21),故默认 4;需要时用 --enum-bytes 0 复核。
|
||||
const EnumBytes = Opts.EnumBytes === undefined ? 4 : Opts.EnumBytes;
|
||||
// EnumInList:list<enum<T>> 里每个元素占多少字节。实测「独立 enum 字段」是 0 字节,
|
||||
// 但列表里的枚举元素看起来仍占 4 字节(否则 P_/Tu 这类带 list<enum> 的模型会整体错位)。
|
||||
// 默认与 EnumBytes 一致;审计时可单独调。
|
||||
const EnumInList = Opts.EnumInList === undefined ? EnumBytes : Opts.EnumInList;
|
||||
// OnField:逐字段回调(诊断用)。参数 {Depth, Name, Type, Start, Next, BitBefore, BitAfter, Bad, Why}
|
||||
// —— 只读观测,不参与解码判定;审计器用它打印「每个字段消费了哪些字节/位」。
|
||||
const OnField = Opts.OnField || null;
|
||||
@@ -176,17 +183,33 @@ function MakeReader(Tables, Options) {
|
||||
let Bad = N.Bad;
|
||||
let Why = '';
|
||||
const Arr = [];
|
||||
// 「元素读不动」的判据在 OptBits 模式下要放宽:元素若是 opt<T> 且为空,值一个字节都不占
|
||||
// (只有 nullmap 里的 1 个 bit,见 src/module_24635.js:412-417 的 Ht.p7g:读到位=1 时直接 return null),
|
||||
// 于是 Next == At 是**合法**的。旧口径把它当「卡住」→ list<opt<T>> 里只要有空元素就整段解不动
|
||||
// (实测 space-100073070/s2c_0001 的 MountedResistancesCC.resistances 就卡在这里)。
|
||||
const ZeroOk = OptBits && /^opt</.test(String(List[1]));
|
||||
// 「元素读不动」的判据在 OptBits 模式下要放宽:
|
||||
// · 元素是 opt<T> 且为空 → 0 字节(只有 nullmap 里的 1 个 bit);
|
||||
// · 元素是 enum<T> → 恒 0 字节(EnumCodecInfo 不上线)。
|
||||
const ElemT = String(List[1]);
|
||||
const ZeroOk = (OptBits && /^opt</.test(ElemT)) || (/^enum</.test(ElemT) && EnumInList === 0);
|
||||
// 列表里的枚举元素按 EnumInList 宽度跳过(独立 enum 字段走 EnumBytes 分支)
|
||||
const ElemEnum = /^enum</.test(ElemT) ? EnumInList : -1;
|
||||
for (let I = 0; I < N.Value; I++) {
|
||||
if (I >= 4096) {
|
||||
Arr.push('<元素过多>');
|
||||
Bad = true;
|
||||
break;
|
||||
}
|
||||
if (ElemEnum > 0) {
|
||||
if (At + ElemEnum > Buf.length) {
|
||||
Arr.push('<越界>');
|
||||
Bad = true;
|
||||
break;
|
||||
}
|
||||
Arr.push('<enum>');
|
||||
At += ElemEnum;
|
||||
continue;
|
||||
}
|
||||
if (ElemEnum === 0) {
|
||||
Arr.push('<enum 不上线>');
|
||||
continue;
|
||||
}
|
||||
const BitBefore = BitIndex;
|
||||
const R = ReadValue(Buf, At, List[1], TypeCodec, Depth + 1);
|
||||
Arr.push(R.Value);
|
||||
@@ -222,8 +245,8 @@ function MakeReader(Tables, Options) {
|
||||
let Bad = N.Bad;
|
||||
const Pairs = [];
|
||||
// 与 list 同源:OptBits 模式下 opt<…> 为空时 0 字节是合法的(见上面 list 分支的注释)
|
||||
const ZeroK = OptBits && /^opt</.test(String(KV[0]));
|
||||
const ZeroV = OptBits && /^opt</.test(String(KV[1]));
|
||||
const ZeroK = (OptBits && /^opt</.test(String(KV[0]))) || /^enum</.test(String(KV[0]));
|
||||
const ZeroV = (OptBits && /^opt</.test(String(KV[1]))) || /^enum</.test(String(KV[1]));
|
||||
for (let I = 0; I < N.Value; I++) {
|
||||
if (I >= 4096) {
|
||||
Pairs.push(['<条目过多>', '']);
|
||||
|
||||
@@ -308,6 +308,8 @@ function Load(NamesDir) {
|
||||
let Off = NmSize;
|
||||
const Ctx = { Bits, Index: 0 };
|
||||
while (Off < Body.length) {
|
||||
// 记录起点时的位游标:诊断用(审计器拿它把 opt 位对齐回本记录)
|
||||
const BitStart = Ctx.Index;
|
||||
if (Ctx.Index >= Bits.length) {
|
||||
Out.Bad = true;
|
||||
Out.Note = 'nullmap 的 ' + Bits.length + ' 个 bit 用完了,帧体还剩 ' + (Body.length - Off) + ' 字节没读';
|
||||
@@ -332,7 +334,7 @@ function Load(NamesDir) {
|
||||
const ObjectId = Body.readBigInt64BE(Off).toString();
|
||||
const MethodId = Body.readBigInt64BE(Off + 8).toString();
|
||||
const R = DecodeRecord(MethodId, Body, Off + 16, undefined, Ctx);
|
||||
Out.Records.push(Object.assign({ ObjectId, At: Off }, R));
|
||||
Out.Records.push(Object.assign({ ObjectId, At: Off, BitStart }, R));
|
||||
if (R.Bad) {
|
||||
Out.Bad = true;
|
||||
Out.Note = '第 ' + Out.Records.length + ' 条记录(methodId=' + MethodId + ')解不动:' + R.Note;
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
// · 必填字段:直接读写
|
||||
// · opt<T> :先从 nullmap 取 1 位(1=null,0=有值),有值才读写 T
|
||||
// · list<T>/map<K,V>:varint 个数 + 逐项
|
||||
// · enum<T>:按 i32 处理。源码上 EnumCodecInfo(module_24635 的 vn → Kt)读写都是空实现,
|
||||
// 但整套改成 0 字节会让回放退化(railgun-battle 多 8 条错 + 回编码不一致 + nullmap 位不足),
|
||||
// 说明表里标成 enum<...> 的字段还有别的东西没建模,宽度不能单独改
|
||||
// · enum<T>:按 i32 处理(与 74 个 enum 字段「全部来自 EnumCodecInfo」的源码结论冲突,见下):
|
||||
// EmissionColorCC 的 4×i32 正好到帧尾,说明该处 enum 必须 0 字节;但全量审计里 enum=0 会让
|
||||
// 21 个模型错位(P_.category 必须 4 字节)。两种口径无法同时满足,暂取整体最优的 i32。
|
||||
// · string/bytes:varint 长度 + 原始字节
|
||||
// · Vector3:3 × f32
|
||||
package Net.Wire
|
||||
|
||||
@@ -162,10 +162,11 @@ def DecodeExpr(Node) -> str:
|
||||
if Kind == "map":
|
||||
return "Wire.ReadMap(R, { " + DecodeExpr(Node[1]) + " }, { " + DecodeExpr(Node[2]) + " })"
|
||||
if Kind == "enum":
|
||||
# 实测口径:按 i32 读。源码上 EnumCodecInfo(module_24635 的 vn → Kt)读写都是空实现,
|
||||
# 但把它改成 0 字节后回放立刻退化(railgun-battle 多出 8 条错、出现回编码不一致、
|
||||
# 并在 space-3074457345817190050 触发“nullmap 位不足”),
|
||||
# 说明现有字段表里标成 enum<...> 的字段还有别的东西没建模,宽度不能单独改。
|
||||
# 枚举宽度在数据上自相矛盾,暂按 i32(最优口径):
|
||||
# · 源码:EnumCodecInfo(vn → Kt)读写都是空实现 → 0 字节;
|
||||
# · 实测 A:EmissionColorCC 的 4×i32 正好到帧尾,threshold 这个 enum 必须 0 字节;
|
||||
# · 实测 B:Item3DCC 之外有 21 个模型在 enum=0 时整体错位(P_ 的 category 必须 4 字节)。
|
||||
# 全量审计:enum=4 → 6 处未命中 / 127 项;enum=0 → 21 处。先保留 4,等客户端位痕迹定案。
|
||||
return "R.ReadInt()"
|
||||
if Kind == "model":
|
||||
return Node[1] + "Codec.Decode(R, N)"
|
||||
|
||||
Reference in new issue
Block a user