- 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
145 lines
6.0 KiB
JavaScript
145 lines
6.0 KiB
JavaScript
// _probe_record.js —— 列出某个 pt 记录里每一项的 modelId / codec / 字节区间 / opt 位数
|
||
//
|
||
// 用途:定位「位游标与客户端不一致」时,究竟是前面哪一项少算了 opt 位。
|
||
// 用法: node _probe_record.js <相对 packets/plain 的文件> [记录序号=1] [最多项=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 File = path.resolve(PROJECT, 'packets', 'plain', process.argv[2]);
|
||
const WantRecord = Number(process.argv[3] || '1');
|
||
const MaxItems = Number(process.argv[4] || '20');
|
||
// 可选:只打印 codec 键包含该子串的项(其余项照常推进,只是不打印)
|
||
const KeyFilter = process.argv[5] || '';
|
||
// 可选:<项序号>:<位数> —— 在第 N 项(1 基)开始前额外推进位游标(模拟「前面的项多读了几位」)
|
||
const InsertArg = (process.argv[6] || '').split(':');
|
||
const InsertAt = InsertArg.length === 2 ? Number(InsertArg[0]) - 1 : -1;
|
||
const InsertBits = InsertArg.length === 2 ? Number(InsertArg[1]) : 0;
|
||
// 可选:--tree 打印每一项的字段树(含嵌套模型 / 列表元素),用来核对「嵌套里到底有几个元素」
|
||
const Tree = process.argv.indexOf('--tree') >= 0;
|
||
|
||
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;
|
||
|
||
let TreeOn = false;
|
||
const Reader = MakeReader(Tables, {
|
||
EnumBytes: 4, EnumInList: 4, OptBits: true, TypeAlias: Alias, StringLiteralOnly: true,
|
||
OnField: (I) => {
|
||
if (!TreeOn) return;
|
||
console.log(' ' + ' '.repeat(I.Depth) + I.Name + ' <' + I.Type + '> ' + I.Start + '→' + I.Next +
|
||
' bit ' + I.BitBefore + '→' + I.BitAfter + (I.Bad ? ' BAD ' + I.Why : ''));
|
||
},
|
||
});
|
||
|
||
// 位流(与 audit 同口径)
|
||
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;
|
||
}
|
||
// 一个 codec(含嵌套)会消耗的 opt 位数(只看字段表)
|
||
function CountOptBits(Key, Depth) {
|
||
if (Depth > 6) return 0;
|
||
const C = Tables.CodecByKey.get(Key);
|
||
if (!C) return 0;
|
||
let N = 0;
|
||
for (const F of C.Fields || []) N += CountTypeOptBits(F.Type, Depth);
|
||
return N;
|
||
}
|
||
function CountTypeOptBits(Type, Depth) {
|
||
if (!Type) return 0;
|
||
let N = 0;
|
||
let T = Type.trim();
|
||
if (T.startsWith('opt<')) { N++; T = T.slice(4, -1).trim(); }
|
||
const M = /^(list|enum|map)<([\s\S]*)>$/.exec(T);
|
||
if (M) {
|
||
const Inner = M[2].trim();
|
||
if (M[1] === 'map') {
|
||
const P = Inner.split(',');
|
||
N += CountTypeOptBits(P[0], Depth + 1) + CountTypeOptBits(P[1], Depth + 1);
|
||
} else N += CountTypeOptBits(Inner, Depth + 1);
|
||
return N;
|
||
}
|
||
const K = FindCodecKey(T);
|
||
if (K) N += CountOptBits(K, Depth + 1);
|
||
return N;
|
||
}
|
||
function FindCodecKey(Name) {
|
||
if (Tables.CodecByKey.has(Name)) return Name;
|
||
for (const [K] of Tables.CodecByKey) if (K.endsWith('::' + Name)) return K;
|
||
return null;
|
||
}
|
||
|
||
const Body = process.argv.indexOf('--body') >= 0
|
||
? fs.readFileSync(File)
|
||
: Link.DecodeFrame(Link.ParseFrames(fs.readFileSync(File))[0]);
|
||
const NmSize = Link.NullmapSize(Body, 0);
|
||
const Bits = NullmapBits(Body.slice(0, NmSize));
|
||
console.log('帧体 ' + Body.length + ' 字节,nullmap ' + NmSize + ' 字节 = ' + Bits.length + ' 位');
|
||
|
||
const Decoded = Semantic.DecodeFrameBody(Body);
|
||
let Seen = 0;
|
||
for (const R of Decoded.Records || []) {
|
||
if (String(R.MethodId) !== '7640916300855664666') continue;
|
||
Seen++;
|
||
if (Seen !== WantRecord) continue;
|
||
const At0 = R.At + 16;
|
||
const C = Body.readInt32BE(At0);
|
||
let At = At0 + 4 + 16 * C;
|
||
const D = Body.readInt32BE(At);
|
||
At += 4;
|
||
Reader.AttachBits(Bits, R.BitStart + 1);
|
||
console.log('记录#' + WantRecord + ' 项数 ' + D + ' 起始 byte=' + At + ' bit=' + (R.BitStart + 1));
|
||
for (let I = 0; I < D && I < MaxItems; I++) {
|
||
if (InsertBits && I === InsertAt) {
|
||
Reader.AttachBits(Bits, Reader.BitCursor().Index + InsertBits);
|
||
console.log(' --- 第 ' + (I + 1) + ' 项前额外推进 ' + InsertBits + ' 位 ---');
|
||
}
|
||
const AtId = At;
|
||
const Id = Body.readBigInt64BE(At).toString();
|
||
const Key = CodecKeyByModelId.get(Id) || '?';
|
||
At += 8;
|
||
const Bit0 = Reader.BitCursor().Index;
|
||
const B0 = At;
|
||
if (Id === '0') {
|
||
At += 8;
|
||
console.log(' [' + String(I + 1).padStart(3) + '] <内联> id@' + AtId + ' byte ' + B0 + '→' + At + ' bit ' + Bit0 + '→' + Reader.BitCursor().Index);
|
||
continue;
|
||
}
|
||
if (Tree && Key && Key.includes(KeyFilter || '::')) {
|
||
console.log(' [树] 第 ' + (I + 1) + ' 项 ' + Key);
|
||
TreeOn = true;
|
||
}
|
||
const Res = Reader.ReadFields(Body, At, Tables.CodecByKey.get(Key), 0);
|
||
TreeOn = false;
|
||
At = Res.Next;
|
||
const Bit1 = Reader.BitCursor().Index;
|
||
const Opt = Key && Key !== '?' ? CountOptBits(Key, 0) : -1;
|
||
if (KeyFilter && !String(Key).includes(KeyFilter)) continue;
|
||
console.log(' [' + String(I + 1).padStart(3) + '] ' + Key + ' model=' + Id + ' id@' + AtId +
|
||
' byte ' + B0 + '→' + At + '(' + (At - B0) + ') bit ' + Bit0 + '→' + Bit1 +
|
||
'(+' + (Bit1 - Bit0) + ') 字段表 opt 共 ' + Opt + (Res.Bad ? ' BAD ' + Res.Why : ''));
|
||
}
|
||
break;
|
||
}
|