import { UnknownFieldHandler, WireType } from "./binary-format-contract"; import { RepeatType, ScalarType } from "./reflection-info"; import { assert } from "./assert"; import { PbLong, PbULong } from "./pb-long"; /** * Writes proto3 messages in binary format using reflection information. * * https://developers.google.com/protocol-buffers/docs/encoding */ export class ReflectionBinaryWriter { constructor(info) { this.info = info; } prepare() { if (!this.fields) { const fieldsInput = this.info.fields ? this.info.fields.concat() : []; this.fields = fieldsInput.sort((a, b) => a.no - b.no); } } /** * Writes the message to binary format. */ write(message, writer, options) { this.prepare(); for (const field of this.fields) { let value, // this will be our field value, whether it is member of a oneof or not emitDefault, // whether we emit the default value (only true for oneof members) repeated = field.repeat, localName = field.localName; // handle oneof ADT if (field.oneof) { const group = message[field.oneof]; if (group.oneofKind !== localName) continue; // if field is not selected, skip value = group[localName]; emitDefault = true; } else { value = message[localName]; emitDefault = false; } // we have handled oneof above. we just have to honor `emitDefault`. switch (field.kind) { case "scalar": case "enum": let T = field.kind == "enum" ? ScalarType.INT32 : field.T; if (repeated) { assert(Array.isArray(value)); if (repeated == RepeatType.PACKED) this.packed(writer, T, field.no, value); else for (const item of value) this.scalar(writer, T, field.no, item, true); } else if (value === undefined) assert(field.opt); else this.scalar(writer, T, field.no, value, emitDefault || field.opt); break; case "message": if (repeated) { assert(Array.isArray(value)); for (const item of value) this.message(writer, options, field.T(), field.no, item); } else { this.message(writer, options, field.T(), field.no, value); } break; case "map": assert(typeof value == 'object' && value !== null); for (const [key, val] of Object.entries(value)) this.mapEntry(writer, options, field, key, val); break; } } let u = options.writeUnknownFields; if (u !== false) (u === true ? UnknownFieldHandler.onWrite : u)(this.info.typeName, message, writer); } mapEntry(writer, options, field, key, value) { writer.tag(field.no, WireType.LengthDelimited); writer.fork(); // javascript only allows number or string for object properties // we convert from our representation to the protobuf type let keyValue = key; switch (field.K) { case ScalarType.INT32: case ScalarType.FIXED32: case ScalarType.UINT32: case ScalarType.SFIXED32: case ScalarType.SINT32: keyValue = Number.parseInt(key); break; case ScalarType.BOOL: assert(key == 'true' || key == 'false'); keyValue = key == 'true'; break; } // write key, expecting key field number = 1 this.scalar(writer, field.K, 1, keyValue, true); // write value, expecting value field number = 2 switch (field.V.kind) { case 'scalar': this.scalar(writer, field.V.T, 2, value, true); break; case 'enum': this.scalar(writer, ScalarType.INT32, 2, value, true); break; case 'message': this.message(writer, options, field.V.T(), 2, value); break; } writer.join(); } message(writer, options, handler, fieldNo, value) { if (value === undefined) return; handler.internalBinaryWrite(value, writer.tag(fieldNo, WireType.LengthDelimited).fork(), options); writer.join(); } /** * Write a single scalar value. */ scalar(writer, type, fieldNo, value, emitDefault) { let [wireType, method, isDefault] = this.scalarInfo(type, value); if (!isDefault || emitDefault) { writer.tag(fieldNo, wireType); writer[method](value); } } /** * Write an array of scalar values in packed format. */ packed(writer, type, fieldNo, value) { if (!value.length) return; assert(type !== ScalarType.BYTES && type !== ScalarType.STRING); // write tag writer.tag(fieldNo, WireType.LengthDelimited); // begin length-delimited writer.fork(); // write values without tags let [, method,] = this.scalarInfo(type); for (let i = 0; i < value.length; i++) writer[method](value[i]); // end length delimited writer.join(); } /** * Get information for writing a scalar value. * * Returns tuple: * [0]: appropriate WireType * [1]: name of the appropriate method of IBinaryWriter * [2]: whether the given value is a default value * * If argument `value` is omitted, [2] is always false. */ scalarInfo(type, value) { let t = WireType.Varint; let m; let i = value === undefined; let d = value === 0; switch (type) { case ScalarType.INT32: m = "int32"; break; case ScalarType.STRING: d = i || !value.length; t = WireType.LengthDelimited; m = "string"; break; case ScalarType.BOOL: d = value === false; m = "bool"; break; case ScalarType.UINT32: m = "uint32"; break; case ScalarType.DOUBLE: t = WireType.Bit64; m = "double"; break; case ScalarType.FLOAT: t = WireType.Bit32; m = "float"; break; case ScalarType.INT64: d = i || PbLong.from(value).isZero(); m = "int64"; break; case ScalarType.UINT64: d = i || PbULong.from(value).isZero(); m = "uint64"; break; case ScalarType.FIXED64: d = i || PbULong.from(value).isZero(); t = WireType.Bit64; m = "fixed64"; break; case ScalarType.BYTES: d = i || !value.byteLength; t = WireType.LengthDelimited; m = "bytes"; break; case ScalarType.FIXED32: t = WireType.Bit32; m = "fixed32"; break; case ScalarType.SFIXED32: t = WireType.Bit32; m = "sfixed32"; break; case ScalarType.SFIXED64: d = i || PbLong.from(value).isZero(); t = WireType.Bit64; m = "sfixed64"; break; case ScalarType.SINT32: m = "sint32"; break; case ScalarType.SINT64: d = i || PbLong.from(value).isZero(); m = "sint64"; break; } return [t, m, i || d]; } }