import {readFileSync} from 'fs'; const version = JSON.parse(readFileSync(new URL('package.json', import.meta.url))).version; export function compile(proto) { return new Function(`const exports = {};\n${compileRaw(proto, {legacy: true})}\nreturn exports;`)(); } export function compileRaw(proto, options = {}) { const context = buildDefaults(buildContext(proto, null), proto.syntax); return `${options.dev ? '' : `// code generated by pbf v${version}\n`}${writeContext(context, options)}`; } function writeContext(ctx, options) { let code = ''; if (ctx._proto.fields) code += writeMessage(ctx, options); if (ctx._proto.values) code += writeEnum(ctx, options); for (let i = 0; i < ctx._children.length; i++) { code += writeContext(ctx._children[i], options); } return code; } function writeMessage(ctx, options) { const fields = ctx._proto.fields; let code = '\n'; if (!options.noRead) { const readName = `read${ctx._name}`; code += `${writeFunctionExport(options, readName)}function ${readName}(pbf, end) { return pbf.readFields(${readName}Field, ${compileDest(ctx)}, end); } function ${readName}Field(tag, obj, pbf) { `; for (let i = 0; i < fields.length; i++) { const field = fields[i]; const {type, name, repeated, oneof, tag} = field; const readCode = compileFieldRead(ctx, field); const packed = willSupportPacked(ctx, field); let fieldRead = type === 'map' ? compileMapRead(readCode, name) : repeated ? (packed ? readCode : `obj.${name}.push(${readCode})`) : `obj.${name} = ${readCode}`; if (oneof) { fieldRead += `; obj.${oneof} = ${JSON.stringify(name)}`; } fieldRead = type === 'map' || oneof ? `{ ${fieldRead}; }` : `${fieldRead};`; code += ` ${i ? 'else ' : ''}if (tag === ${tag}) ${fieldRead}\n`; } code += '}\n'; } if (!options.noWrite) { const writeName = `write${ctx._name}`; code += `${writeFunctionExport(options, writeName)}function ${writeName}(obj, pbf) {\n`; for (const field of fields) { const writeCode = field.repeated && !isPacked(field) ? compileRepeatedWrite(ctx, field) : field.type === 'map' ? compileMapWrite(ctx, field) : compileFieldWrite(ctx, field, `obj.${field.name}`); code += getDefaultWriteTest(ctx, field); code += `${writeCode};\n`; } code += '}\n'; } return code; } function writeFunctionExport({legacy}, name) { return legacy ? `exports.${name} = ${name};\n` : 'export '; } function getEnumValues(ctx) { const enums = {}; const ids = Object.keys(ctx._proto.values); for (let i = 0; i < ids.length; i++) { enums[ids[i]] = ctx._proto.values[ids[i]].value; } return enums; } function writeEnum(ctx, {legacy}) { const enums = JSON.stringify(getEnumValues(ctx), null, 4); const name = ctx._name; return `\n${legacy ? `const ${name} = exports.${name}` : `export const ${name}`} = ${enums};\n`; } function compileDest(ctx) { const props = new Set(); for (const {name, oneof} of ctx._proto.fields) { props.add(`${name}: ${JSON.stringify(ctx._defaults[name])}`); if (oneof) props.add(`${oneof }: undefined`); } return `{${[...props].join(', ')}}`; } function isEnum(type) { return type && type._proto.values; } function getType(ctx, field) { if (field.type === 'map') { return ctx[getMapMessageName(field.tag)]; } const path = field.type.split('.'); return path.reduce((ctx, name) => ctx && ctx[name], ctx); } function fieldShouldUseStringAsNumber(field) { if (field.options.jstype === 'JS_STRING') { switch (field.type) { case 'float': case 'double': case 'uint32': case 'uint64': case 'int32': case 'int64': case 'sint32': case 'sint64': case 'fixed32': case 'fixed64': case 'sfixed32': case 'sfixed64': return true; default: return false; } } return false; } function compileFieldRead(ctx, field) { const type = getType(ctx, field); if (type) { if (type._proto.fields) return `read${type._name}(pbf, pbf.readVarint() + pbf.pos)`; if (!isEnum(type)) throw new Error(`Unexpected type: ${type._name}`); } const fieldType = isEnum(type) ? 'enum' : field.type; let prefix = 'pbf.read'; const signed = fieldType === 'int32' || fieldType === 'int64' ? 'true' : ''; let suffix = `(${signed})`; if (willSupportPacked(ctx, field)) { prefix += 'Packed'; suffix = `(obj.${field.name}${signed ? `, ${signed}` : ''})`; } if (fieldShouldUseStringAsNumber(field)) { suffix += '.toString()'; } switch (fieldType) { case 'string': return `${prefix}String${suffix}`; case 'float': return `${prefix}Float${suffix}`; case 'double': return `${prefix}Double${suffix}`; case 'bool': return `${prefix}Boolean${suffix}`; case 'enum': case 'uint32': case 'uint64': case 'int32': case 'int64': return `${prefix}Varint${suffix}`; case 'sint32': case 'sint64': return `${prefix}SVarint${suffix}`; case 'fixed32': return `${prefix}Fixed32${suffix}`; case 'fixed64': return `${prefix}Fixed64${suffix}`; case 'sfixed32': return `${prefix}SFixed32${suffix}`; case 'sfixed64': return `${prefix}SFixed64${suffix}`; case 'bytes': return `${prefix}Bytes${suffix}`; default: throw new Error(`Unexpected type: ${field.type}`); } } function compileFieldWrite(ctx, field, name) { let prefix = 'pbf.write'; if (isPacked(field)) prefix += 'Packed'; if (fieldShouldUseStringAsNumber(field)) { if (field.type === 'float' || field.type === 'double') { name = `parseFloat(${name})`; } else { name = `parseInt(${name}, 10)`; } } const postfix = `${isPacked(field) ? '' : 'Field'}(${field.tag}, ${name})`; const type = getType(ctx, field); if (type) { if (type._proto.fields) return `${prefix}Message(${field.tag}, write${type._name}, ${name})`; if (type._proto.values) return `${prefix}Varint${postfix}`; throw new Error(`Unexpected type: ${type._name}`); } switch (field.type) { case 'string': return `${prefix}String${postfix}`; case 'float': return `${prefix}Float${postfix}`; case 'double': return `${prefix}Double${postfix}`; case 'bool': return `${prefix}Boolean${postfix}`; case 'enum': case 'uint32': case 'uint64': case 'int32': case 'int64': return `${prefix}Varint${postfix}`; case 'sint32': case 'sint64': return `${prefix}SVarint${postfix}`; case 'fixed32': return `${prefix}Fixed32${postfix}`; case 'fixed64': return `${prefix}Fixed64${postfix}`; case 'sfixed32': return `${prefix}SFixed32${postfix}`; case 'sfixed64': return `${prefix}SFixed64${postfix}`; case 'bytes': return `${prefix}Bytes${postfix}`; default: throw new Error(`Unexpected type: ${field.type}`); } } function compileMapRead(readCode, name) { return `const {key, value} = ${readCode}; obj.${name}[key] = value`; } function compileRepeatedWrite(ctx, field) { return `for (const item of obj.${field.name}) ${ compileFieldWrite(ctx, field, 'item')}`; } function compileMapWrite(ctx, field) { const name = `obj.${field.name}`; return `for (const key of Object.keys(${name})) ${ compileFieldWrite(ctx, field, `{key, value: ${name}[key]}`)}`; } function getMapMessageName(tag) { return `_FieldEntry${tag}`; } function getMapField(name, type, tag) { return { name, type, tag, map: null, oneof: null, required: false, repeated: false, options: {} }; } function getMapMessage(field) { return { name: getMapMessageName(field.tag), enums: [], messages: [], extensions: null, fields: [ getMapField('key', field.map.from, 1), getMapField('value', field.map.to, 2) ] }; } function buildContext(proto, parent) { const obj = Object.create(parent); obj._proto = proto; obj._children = []; obj._defaults = {}; if (parent) { parent[proto.name] = obj; if (parent._name) { obj._name = parent._name + proto.name; } else { obj._name = proto.name; } } for (let i = 0; proto.enums && i < proto.enums.length; i++) { obj._children.push(buildContext(proto.enums[i], obj)); } for (let i = 0; proto.messages && i < proto.messages.length; i++) { obj._children.push(buildContext(proto.messages[i], obj)); } for (let i = 0; proto.fields && i < proto.fields.length; i++) { if (proto.fields[i].type === 'map') { obj._children.push(buildContext(getMapMessage(proto.fields[i]), obj)); } } return obj; } function getDefaultValue(field, value) { // Defaults not supported for repeated fields if (field.repeated) return []; let convertToStringIfNeeded = function (val) { return val; }; if (fieldShouldUseStringAsNumber(field)) { convertToStringIfNeeded = function (val) { return val.toString(); }; } switch (field.type) { case 'float': case 'double': return convertToStringIfNeeded(value ? parseFloat(value) : 0); case 'uint32': case 'uint64': case 'int32': case 'int64': case 'sint32': case 'sint64': case 'fixed32': case 'fixed64': case 'sfixed32': case 'sfixed64': return convertToStringIfNeeded(value ? parseInt(value, 10) : 0); case 'string': return value || ''; case 'bool': return value === 'true'; case 'map': return {}; default: return undefined; } } function willSupportPacked(ctx, field) { const fieldType = isEnum(getType(ctx, field)) ? 'enum' : field.type; switch (field.repeated && fieldType) { case 'float': case 'double': case 'uint32': case 'uint64': case 'int32': case 'int64': case 'sint32': case 'sint64': case 'fixed32': case 'fixed64': case 'sfixed32': case 'enum': case 'bool': return true; } return false; } function setPackedOption(ctx, field, syntax) { // No default packed in older protobuf versions if (syntax < 3) return; // Packed option already set if (field.options.packed !== undefined) return; // Not a packed field type if (!willSupportPacked(ctx, field)) return; field.options.packed = 'true'; } function setDefaultValue(ctx, field, syntax) { const options = field.options; const type = getType(ctx, field); const enumValues = type && type._proto.values && getEnumValues(type); // Proto3 does not support overriding defaults const explicitDefault = syntax < 3 ? options.default : undefined; // Set default for enum values if (enumValues && !field.repeated) { ctx._defaults[field.name] = enumValues[explicitDefault] || 0; } else { ctx._defaults[field.name] = getDefaultValue(field, explicitDefault); } } function buildDefaults(ctx, syntax) { const proto = ctx._proto; for (let i = 0; i < ctx._children.length; i++) { buildDefaults(ctx._children[i], syntax); } if (proto.fields) { for (let i = 0; i < proto.fields.length; i++) { setPackedOption(ctx, proto.fields[i], syntax); setDefaultValue(ctx, proto.fields[i], syntax); } } return ctx; } function getDefaultWriteTest(ctx, field) { const def = ctx._defaults[field.name]; const type = getType(ctx, field); let code = ` if (obj.${field.name}`; if (!field.repeated && (!type || !type._proto.fields)) { if (def === undefined || def || field.oneof) { code += ' != null'; } if (def) { code += ` && obj.${field.name} !== ${JSON.stringify(def)}`; } } return `${code}) `; } function isPacked(field) { return field.options.packed === 'true'; }