-- pb.from_pb: build runtime modules from a binary FileDescriptorSet. -- -- Complements pb.parse (which consumes .proto source text) by accepting the -- output of `protoc --descriptor_set_out=...`. The pipeline: -- bytes -> FileDescriptorSet decoded via descriptor.proto descriptors -- -> per-file AST (same shape as pb.parser produces) -- -> per-file module via pb.dynamic.build -- -- Returns: -- { -- files = {[file_name] = module, ...}, -- one entry per FileDescriptorProto -- order = {file_name, ...}, -- declaration order -- lookup = function(full_name) -> descriptor | nil, -- } local codec = require('pb.codec') local dynamic = require('pb.dynamic') local descpb = require('pb.descriptor_pb') local M = {} -- FieldDescriptorProto.Type wire values -> proto3 type-name strings. local TYPE_NAMES = { [1] = 'double', [2] = 'float', [3] = 'int64', [4] = 'uint64', [5] = 'int32', [6] = 'fixed64', [7] = 'fixed32', [8] = 'bool', [9] = 'string', -- 10 = TYPE_GROUP (proto2 only; not supported) [11] = 'message', [12] = 'bytes', [13] = 'uint32', [14] = 'enum', [15] = 'sfixed32', [16] = 'sfixed64', [17] = 'sint32', [18] = 'sint64', } local LABEL_REPEATED = 3 local function strip_dot(name) if name == nil then return nil end return (name:gsub('^%.', '')) end -- Collect the full names of nested map-entry messages so the parent -- translator knows which TYPE_MESSAGE fields are really map fields. Returns -- {[full_name] = {key_type=, value_type=}, ...}. local function collect_map_entries(msg_proto, scope, out) out = out or {} local full = scope == '' and msg_proto.name or (scope .. '.' .. msg_proto.name) if msg_proto.options and msg_proto.options.map_entry then local key_t, val_t local key_typename, val_typename for _, f in ipairs(msg_proto.field or {}) do local tn = TYPE_NAMES[f.type] if f.name == 'key' then if tn == 'message' or tn == 'enum' then key_typename = strip_dot(f.type_name) else key_t = tn end elseif f.name == 'value' then if tn == 'message' or tn == 'enum' then val_typename = strip_dot(f.type_name) else val_t = tn end end end out[full] = { key_type = key_t or key_typename, value_type = val_t or val_typename, } end for _, nested in ipairs(msg_proto.nested_type or {}) do collect_map_entries(nested, full, out) end return out end -- Translate a FieldDescriptorProto into an AST field. local function translate_field(f, oneofs_decl, map_entries) local type_id = f.type local typename = TYPE_NAMES[type_id] local repeated = (f.label == LABEL_REPEATED) -- Map fields are represented as repeated synthetic-entry messages. if repeated and typename == 'message' then local entry_full = strip_dot(f.type_name) local map_info = entry_full and map_entries[entry_full] if map_info then return { name = f.name, id = f.number, kind = 'map', key_type = map_info.key_type, value_type = map_info.value_type, }, true -- second return signals "skip the entry message" end end -- Resolve the type string the AST consumer expects. local ast_type if typename == 'message' or typename == 'enum' then ast_type = strip_dot(f.type_name) else ast_type = typename end local entry = { name = f.name, id = f.number, type = ast_type, } if repeated then entry.repeated = true if f.options and f.options.packed ~= nil then entry.packed = f.options.packed end end if f.proto3_optional then entry.optional = true elseif f.oneof_index ~= nil then -- oneof_index references oneof_decl[]. It is zero-based in -- descriptor.proto wire form, but we get Lua-decoded plain ints. local decl = oneofs_decl[f.oneof_index + 1] if decl then entry.oneof = decl.name end end return entry, false end -- Translate a DescriptorProto into the AST message shape produced by -- pb.parser. Returns {name=, fields=, nested_messages=, nested_enums=, oneofs=}. local function translate_message(msg_proto, scope, map_entries) local full = scope == '' and msg_proto.name or (scope .. '.' .. msg_proto.name) map_entries = map_entries or collect_map_entries(msg_proto, scope) local ast = { name = msg_proto.name, fields = {}, nested_messages = {}, nested_enums = {}, oneofs = {}, } local oneofs_decl = msg_proto.oneof_decl or {} -- Group oneof field names by oneof_decl index, excluding proto3_optional -- synthetic oneofs. local oneof_groups = {} -- {[idx] = {field_names...}} for _, f in ipairs(msg_proto.field or {}) do if f.oneof_index ~= nil and not f.proto3_optional then local idx = f.oneof_index + 1 oneof_groups[idx] = oneof_groups[idx] or {} table.insert(oneof_groups[idx], f.name) end end for _, f in ipairs(msg_proto.field or {}) do local entry, _ = translate_field(f, oneofs_decl, map_entries) table.insert(ast.fields, entry) end -- Emit oneofs in oneof_decl order. for i, decl in ipairs(oneofs_decl) do local names = oneof_groups[i] if names and #names > 0 then table.insert(ast.oneofs, {name = decl.name, fields = names}) end end -- Nested types — skip map-entry synthetic messages. for _, nested in ipairs(msg_proto.nested_type or {}) do local nested_full = full .. '.' .. nested.name if not map_entries[nested_full] then table.insert(ast.nested_messages, translate_message(nested, full, map_entries)) end end for _, en in ipairs(msg_proto.enum_type or {}) do local values = {} for _, v in ipairs(en.value or {}) do values[v.name] = v.number or 0 end table.insert(ast.nested_enums, {name = en.name, values = values}) end return ast end -- Translate a FileDescriptorProto into the AST shape pb.dynamic.build expects. local function translate_file(file_proto) local ast = { syntax = file_proto.syntax ~= '' and file_proto.syntax or 'proto3', package = file_proto.package or '', imports = {}, messages = {}, enums = {}, services = {}, } for _, dep in ipairs(file_proto.dependency or {}) do table.insert(ast.imports, dep) end -- Gather top-level map entries (none, by construction — map entries are -- always nested — but we keep the recursion uniform). for _, msg in ipairs(file_proto.message_type or {}) do table.insert(ast.messages, translate_message(msg, ast.package)) end for _, en in ipairs(file_proto.enum_type or {}) do local values = {} for _, v in ipairs(en.value or {}) do values[v.name] = v.number or 0 end table.insert(ast.enums, {name = en.name, values = values}) end for _, svc in ipairs(file_proto.service or {}) do local methods = {} for _, m in ipairs(svc.method or {}) do table.insert(methods, { name = m.name, input = strip_dot(m.input_type), output = strip_dot(m.output_type), client_streaming = m.client_streaming or false, server_streaming = m.server_streaming or false, }) end table.insert(ast.services, {name = svc.name, methods = methods}) end return ast end -- Build a name-lookup that walks every module and exposes _descriptor -- entries by their proto full name. local function build_lookup(files, order) local by_full = {} for _, fname in ipairs(order) do local module = files[fname] -- Re-create the descriptor index by iterating over the module's -- _descriptor entries. The descriptor's .name field is the proto -- full name (e.g., "pkg.sub.Foo"). for k, v in pairs(module) do if type(k) == 'string' and k:match('_descriptor$') and type(v) == 'table' then if v.name then by_full[v.name] = v end end end end return function(full_name) return by_full[full_name] end end -- Public entry. function M.parse(bytes) local set = codec.decode(descpb.FileDescriptorSet, bytes) local files = {} local order = {} for _, f in ipairs(set.file or {}) do local ast = translate_file(f) local module = dynamic.build(ast) local name = f.name ~= '' and f.name or ('file_' .. tostring(#order + 1)) files[name] = module table.insert(order, name) end return { files = files, order = order, lookup = build_lookup(files, order), } end return M