// 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 = varint 元素数 + 元素… // map = varint 条数 + 条数 × (K, V)(读侧同源,见 lib_space_codec.js 头部 map 那段) // opt = 1 字节标志(0 = 有值,非 0 = 空)+ 有值时紧跟 T // —— 这是「字段表」那条路径的近似口径。真实线形是 nullmap 里的 1 个 bit(见下 OptBits)。 // opt(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(OptBits 模式 + AttachBits)= 「空/非空」那个 bit **写回**调用方给的 nullmap 比特流 // (就地覆盖 Bits[Index],见下 PutBit)。语义层重建 nullmap 时按这条路径走: // 位图 = 形态位 + 各 opt 位,读写必须消费同一顺序,否则位就串了。 // enum = 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 ({ Name: F.Field, Type: F.Type, TypeCodec: F.TypeCodec || '' })); return { Codec: null, Fields }; } module.exports = { MakeWriter, ResolveFields, MAX_DEPTH };