工具链(3DTank/tools/):extract(模块提取)、mine_evidence/apply_names(命名证据与别名回填)、extract_codec_fields(字段表 + 全库槽位类型 + PrimClassWire/ResourceClasses)、mine_downlink(下行字段表)、extract_space_init/rebase_space_init/audit_space_init(空间模板与记录切分审计)、extract_space_session(把真实会话切成按上行触发的回放模板)、pair_flow(请求→应答配对)、probe_containers、ingest/decrypt_frames(抓包入库与解密)、serve/fetch_assets(本地站点镜像)、import_db(抓包对象仓库导入 MySQL)。 服务器(3DTank/tools/server/):link/entry/codec/container/dispatch/world/space/semantic(协议层与 lt/pt/zt 三个容器布局)、session(会话回放,顺序推进)、db + schema.sql(MySQL 连接层与对象仓库表结构)、store/responder(查库组帧与定向应答;接管范围默认只开 ba.lcr)。 同时修 pair_flow 的时间轴偏移:timeline_c2s 与 timeline_s2c 的 0 点相差 144~4453ms,改前配对整体偏约 2 秒(实测 space-100073070 偏 2084ms),改后 22.4% 的触发者被纠正。 src/、src_named/、packets/、3DTank/local/(含数据库口令)不进库,可用上述工具重新生成。
239 lines
12 KiB
JavaScript
239 lines
12 KiB
JavaScript
// codec.js - 空间层负载的字段级编码器 MakeWriter(写包用,与 lib_space_codec 的 MakeReader 严格对称)
|
||
//
|
||
// 编码规则(出处同 lib_space_codec.js 头部注释,module_24635 的序列化器实现):
|
||
// bool/i8=1B i16=2B i32/f32=4B i64/f64=8B(大端)
|
||
// string/bytes = varint 长度 + 原始字节
|
||
// list<T> = varint 元素数 + 元素…
|
||
// map<K, V> = varint 条数 + 条数 × (K, V)(读侧同源,见 lib_space_codec.js 头部 map 那段)
|
||
// opt<T> = 1 字节标志(0 = 有值,非 0 = 空)+ 有值时紧跟 T
|
||
// —— 这是「字段表」那条路径的近似口径。真实线形是 nullmap 里的 1 个 bit(见下 OptBits)。
|
||
// opt<T>(OptBits 模式)= 不写任何标志字节:空值写 0 字节,有值只写内层 T。
|
||
// 为什么写侧不用管那个 bit:位在 nullmap 里,而 nullmap 由帧级 NullmapHex 原样带上
|
||
// (见 space.js:26-32、semantic.js:267-284);写侧只要保证「空值真的一个字节都不写」
|
||
// (src/module_24635.js:412-414 的 Ht.o7g:null 时只写位、不写值)。
|
||
// opt<T>(OptBits 模式 + AttachBits)= 「空/非空」那个 bit **写回**调用方给的 nullmap 比特流
|
||
// (就地覆盖 Bits[Index],见下 PutBit)。语义层重建 nullmap 时按这条路径走:
|
||
// 位图 = 形态位 + 各 opt 位,读写必须消费同一顺序,否则位就串了。
|
||
// enum<T> = 4 字节(大端无符号;读侧按同一宽度拼出,可能为负,这里用 asUintN 还原)
|
||
// 模型类 = 就地展开该模型 codec 的字段(无长度前缀)
|
||
// 取值形状:**与 MakeReader 的输出一一对应**(i64 = 十进制字符串、enum = 'enum#N' 或数字、
|
||
// bytes = Buffer/0x 十六进制串、opt 空 = null),这样「解出来再写回去」能逐字节还原。
|
||
// 缺字段:opt 缺 = 空;非 opt 缺 → 直接抛错(不做静默兜底)。
|
||
// 未实现:容器三编码(lt/pt/zt,见 module_45686)与模块内建 codec(Ea=变长数组、nh 等)→ 抛错暴露。
|
||
'use strict';
|
||
|
||
const MAX_DEPTH = 8;
|
||
const HEX = /^0x[0-9a-fA-F]*$/;
|
||
const { SplitTypeArgs } = require('../lib_space_codec');
|
||
|
||
function MakeWriter(Tables, Options) {
|
||
const Opts = Options || {};
|
||
const EnumBytes = Opts.EnumBytes === undefined ? 4 : Opts.EnumBytes;
|
||
const OptBits = !!Opts.OptBits;
|
||
const TypeAlias = Opts.TypeAlias || null;
|
||
// 与 MakeReader 的 StringLiteralOnly 严格同源:开了就把 string 一律当字面 UTF-8 写,
|
||
// 不再把 '0x…' 当二进制转义(否则字面量 "0xff8000" 会被写成 3 个裸字节)。
|
||
const StringLiteralOnly = !!Opts.StringLiteralOnly;
|
||
const CodecByKey = Tables.CodecByKey;
|
||
const CodecByModelName = Tables.CodecByModelName || new Map();
|
||
const WireClasses = Tables.WireClasses || new Map();
|
||
|
||
// nullmap 比特流的**写**游标(与 lib_space_codec.js:130-148 的读游标同源、同顺序)。
|
||
// 由调用方(semantic.js:EncodeFrameBody / container.js:Write)AttachBits 进来:
|
||
// AttachBits(Bits, Index) → Bits 是要就地覆盖的位数组(长度固定 = 原 nullmap 的位图位数)
|
||
// DetachBits() → 断开(opt 回到「不写位」的老路径)
|
||
let BitStream = null;
|
||
let BitIndex = 0;
|
||
function AttachBits(Bits, Index) {
|
||
BitStream = Bits;
|
||
BitIndex = Index || 0;
|
||
}
|
||
function DetachBits() {
|
||
BitStream = null;
|
||
BitIndex = 0;
|
||
}
|
||
function BitCursor() {
|
||
return { Bits: BitStream, Index: BitIndex };
|
||
}
|
||
// 写 1 个 opt 位。位不够 = nullmap 比负载短 → 直接抛错(不静默丢弃,否则组帧出来是错的)
|
||
function PutBit(V) {
|
||
if (!BitStream) throw new Error('内部错误:没有 attach nullmap 比特流却要写 opt 位');
|
||
if (BitIndex >= BitStream.length) throw new Error('nullmap 位图用完(已写 ' + BitIndex + ' 位,容量 ' + BitStream.length + ')');
|
||
BitStream[BitIndex++] = V ? 1 : 0;
|
||
}
|
||
|
||
function WriteVarint(N) {
|
||
const V = Number(N);
|
||
if (!Number.isFinite(V) || V < 0) throw new Error('varint 值非法: ' + N);
|
||
if (V < 0x80) return Buffer.from([V]);
|
||
if (V < 0x4000) return Buffer.from([0x80 | (V >> 8), V & 0xff]);
|
||
if (V < 0x400000) return Buffer.from([0xc0 | (V >> 16), (V >> 8) & 0xff, V & 0xff]);
|
||
throw new Error('varint 过大: ' + V);
|
||
}
|
||
|
||
function WriteString(S) {
|
||
// 读侧对「不是合法 UTF-8 的字符串」会给 0x 十六进制形式,这里原样还原成字节
|
||
// (StringLiteralOnly 模式下读侧不会产生这种形式,所以不认它)
|
||
if (!StringLiteralOnly && typeof S === 'string' && HEX.test(S)) {
|
||
const Raw = Buffer.from(S.slice(2), 'hex');
|
||
return Buffer.concat([WriteVarint(Raw.length), Raw]);
|
||
}
|
||
const B = Buffer.from(String(S), 'utf8');
|
||
return Buffer.concat([WriteVarint(B.length), B]);
|
||
}
|
||
|
||
function ToBytes(V) {
|
||
if (Buffer.isBuffer(V)) return V;
|
||
if (V === null || V === undefined) return Buffer.alloc(0);
|
||
if (typeof V === 'string' && HEX.test(V)) return Buffer.from(V.slice(2), 'hex');
|
||
if (Array.isArray(V)) return Buffer.from(V);
|
||
throw new Error('bytes 值无法识别: ' + typeof V);
|
||
}
|
||
|
||
function WriteValue(Type, TypeCodec, Value, Depth) {
|
||
if (Depth > MAX_DEPTH) throw new Error('嵌套超过 ' + MAX_DEPTH + ' 层');
|
||
if (Type === undefined || Type === null || Type === '' || Type === '?') throw new Error('字段类型缺失');
|
||
const T = String(Type);
|
||
|
||
const Opt = /^opt<([\s\S]+)>$/.exec(T);
|
||
if (Opt) {
|
||
if (OptBits) {
|
||
// 空值 = 负载里 0 字节(bit 在 nullmap 里);有值只写内层
|
||
const Empty = Value === null || Value === undefined;
|
||
// 有写游标 ⇒ 把「空/非空」写回 nullmap 比特流(src/module_24635.js:412-414 的 Ht.o7g:
|
||
// 写侧对空值只写 1 个 bit 的 !0、不写值)。注意「写位」与「写值」的顺序必须与读侧一致。
|
||
if (BitStream) PutBit(Empty);
|
||
if (Empty) return Buffer.alloc(0);
|
||
return WriteValue(Opt[1], TypeCodec, Value, Depth + 1);
|
||
}
|
||
if (Value === null || Value === undefined) return Buffer.from([1]);
|
||
return Buffer.concat([Buffer.from([0]), WriteValue(Opt[1], TypeCodec, Value, Depth + 1)]);
|
||
}
|
||
const List = /^list<([\s\S]+)>$/.exec(T);
|
||
if (List) {
|
||
if (!Array.isArray(Value)) throw new Error('list 值必须是数组: ' + typeof Value);
|
||
const Parts = [WriteVarint(Value.length)];
|
||
for (const X of Value) Parts.push(WriteValue(List[1], TypeCodec, X, Depth + 1));
|
||
return Buffer.concat(Parts);
|
||
}
|
||
const Enum = /^enum<([\s\S]+)>$/.exec(T);
|
||
if (Enum) {
|
||
if (EnumBytes === 0) return Buffer.alloc(0);
|
||
const N = typeof Value === 'string' ? Number(String(Value).replace(/^enum#/, '')) : Number(Value);
|
||
if (!Number.isFinite(N)) throw new Error('enum 值无法识别: ' + Value);
|
||
const U = BigInt.asUintN(EnumBytes * 8, BigInt(Math.trunc(N)));
|
||
const Out = Buffer.alloc(EnumBytes);
|
||
Out.writeUIntBE(Number(U), 0, EnumBytes);
|
||
return Out;
|
||
}
|
||
const Map = /^map<([\s\S]+)>$/.exec(T);
|
||
if (Map) {
|
||
// 与读侧严格对称:varint 条数 + 逐条 (key, value)(出处见 lib_space_codec.js 头部 map 那段)。
|
||
// 值形状必须是 [[key, value], …]——不做「对象转条目」的兜底,那会在键重复/非字符串键时静默丢字节。
|
||
const KV = SplitTypeArgs(Map[1]);
|
||
if (!Array.isArray(Value)) throw new Error('map 值必须是 [[key, value], …]:' + typeof Value);
|
||
const Parts = [WriteVarint(Value.length)];
|
||
for (const Pair of Value) {
|
||
if (!Array.isArray(Pair) || Pair.length !== 2) throw new Error('map 条目必须是 [key, value]:' + JSON.stringify(Pair));
|
||
Parts.push(WriteValue(KV[0], TypeCodec, Pair[0], Depth + 1));
|
||
Parts.push(WriteValue(KV[1], TypeCodec, Pair[1], Depth + 1));
|
||
}
|
||
return Buffer.concat(Parts);
|
||
}
|
||
|
||
switch (T) {
|
||
case 'bool': {
|
||
const Out = Buffer.alloc(1);
|
||
Out[0] = Value ? 1 : 0;
|
||
return Out;
|
||
}
|
||
case 'i8': {
|
||
const Out = Buffer.alloc(1);
|
||
Out.writeInt8(Number(Value) | 0, 0);
|
||
return Out;
|
||
}
|
||
case 'i16': {
|
||
const Out = Buffer.alloc(2);
|
||
Out.writeInt16BE(Number(Value) | 0, 0);
|
||
return Out;
|
||
}
|
||
case 'i32': {
|
||
const Out = Buffer.alloc(4);
|
||
Out.writeInt32BE(Number(Value) | 0, 0);
|
||
return Out;
|
||
}
|
||
case 'i64': {
|
||
const Out = Buffer.alloc(8);
|
||
Out.writeBigInt64BE(BigInt(Value), 0);
|
||
return Out;
|
||
}
|
||
case 'f32': {
|
||
const Out = Buffer.alloc(4);
|
||
Out.writeFloatBE(Number(Value), 0);
|
||
return Out;
|
||
}
|
||
case 'f64': {
|
||
const Out = Buffer.alloc(8);
|
||
Out.writeDoubleBE(Number(Value), 0);
|
||
return Out;
|
||
}
|
||
case 'string':
|
||
return WriteString(Value);
|
||
case 'bytes':
|
||
case 'array': {
|
||
const B = ToBytes(Value);
|
||
return Buffer.concat([WriteVarint(B.length), B]);
|
||
}
|
||
default:
|
||
break;
|
||
}
|
||
|
||
// 模型类:就地展开它自己的 codec 字段
|
||
if (TypeCodec && CodecByKey.has(TypeCodec)) return WriteFields(CodecByKey.get(TypeCodec), Value, Depth + 1);
|
||
if (CodecByModelName.has(T)) return WriteFields(CodecByModelName.get(T), Value, Depth + 1);
|
||
if (WireClasses.has(T)) return WriteValue(WireClasses.get(T), '', Value, Depth + 1);
|
||
// 容器层给的「裸类名 → 线上基础类型」直译表(与 MakeReader 的 TypeAlias 严格同源)
|
||
if (TypeAlias && Object.prototype.hasOwnProperty.call(TypeAlias, T)) return WriteValue(TypeAlias[T], '', Value, Depth + 1);
|
||
throw new Error('字段类型没有对应 codec: ' + T);
|
||
}
|
||
|
||
function WriteFields(Codec, Obj, Depth) {
|
||
const Parts = [];
|
||
for (const F of (Codec && Codec.Fields) || []) {
|
||
const Key = F.Name === '(匿名)' || F.Name === '?' ? '(匿名#' + F.Order + ')' : F.Name;
|
||
const Has = Obj !== null && Obj !== undefined && Object.prototype.hasOwnProperty.call(Obj, Key);
|
||
if (!Has) {
|
||
if (!/^opt</.test(String(F.Type))) {
|
||
throw new Error('缺少必填字段 ' + Key + '(codec=' + ((Codec && Codec.ModelName) || (Codec && Codec.Codec)) + ')');
|
||
}
|
||
Parts.push(WriteValue(F.Type, F.TypeCodec, null, Depth));
|
||
continue;
|
||
}
|
||
Parts.push(WriteValue(F.Type, F.TypeCodec, Obj[Key], Depth));
|
||
}
|
||
return Buffer.concat(Parts);
|
||
}
|
||
|
||
function WriteByKey(CodecKey, Obj) {
|
||
const C = CodecByKey.get(CodecKey);
|
||
if (!C) throw new Error('表里没有这个 codec: ' + CodecKey);
|
||
return WriteFields(C, Obj, 0);
|
||
}
|
||
|
||
return { WriteValue, WriteFields, WriteByKey, WriteVarint, WriteString, EnumBytes, OptBits, AttachBits, DetachBits, BitCursor };
|
||
}
|
||
|
||
// 按 methodId 取「描述这条命令负载」的字段表(服务端与自检共用同一套解析,避免两处口径不一):
|
||
// 优先 names/codec_fields.json 按 methodId 反查到的 codec(带类型),否则回落到
|
||
// names/uplink_table.json 里命令自带的字段列表(只有名字与类型串)。
|
||
// 实测两张表的 methodId 基本不重合(上行 360 条走 uplink_table,下行 codec 432 个走 codec_fields),
|
||
// 所以两个来源都得看。
|
||
function ResolveFields(Tables, MethodId, Cmd) {
|
||
const Codec = Tables.CodecByMethod.get(String(MethodId));
|
||
if (Codec) return { Codec, Fields: Codec.Fields || [] };
|
||
const Fields = ((Cmd && Cmd.Fields) || []).map((F) => ({ Name: F.Field, Type: F.Type, TypeCodec: F.TypeCodec || '' }));
|
||
return { Codec: null, Fields };
|
||
}
|
||
|
||
module.exports = { MakeWriter, ResolveFields, MAX_DEPTH };
|