/**
* Copyright (C) 2009-2013 FoundationDB, LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.foundationdb.protobuf;
import com.google.protobuf.DescriptorProtos.DescriptorProto;
import com.google.protobuf.DescriptorProtos.EnumDescriptorProto;
import com.google.protobuf.DescriptorProtos.EnumValueDescriptorProto;
import com.google.protobuf.DescriptorProtos.FieldDescriptorProto;
import com.google.protobuf.DescriptorProtos.FieldDescriptorProto.Label;
import com.google.protobuf.DescriptorProtos.FieldDescriptorProto.Type;
import com.google.protobuf.DescriptorProtos.FileDescriptorProto;
import com.google.protobuf.DescriptorProtos.FileDescriptorSet;
import com.google.protobuf.DescriptorProtos.MethodDescriptorProto;
import com.google.protobuf.DescriptorProtos.ServiceDescriptorProto;
import com.google.protobuf.MessageOrBuilder;
import com.google.protobuf.Descriptors.FieldDescriptor;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.io.BufferedWriter;
import java.io.FileInputStream;
import java.io.Flushable;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
public class ProtobufDecompiler
{
protected static final Map<Label,String> LABELS = new HashMap<>();
protected static final Map<Type,String> TYPES = new HashMap<>();
static {
LABELS.put(Label.LABEL_OPTIONAL, "optional");
LABELS.put(Label.LABEL_REPEATED, "repeated");
LABELS.put(Label.LABEL_REQUIRED, "required");
// Simple types only.
TYPES.put(Type.TYPE_BOOL, "bool");
TYPES.put(Type.TYPE_BYTES, "bytes");
TYPES.put(Type.TYPE_DOUBLE, "double");
TYPES.put(Type.TYPE_FIXED32, "fixed32");
TYPES.put(Type.TYPE_FIXED64, "fixed64");
TYPES.put(Type.TYPE_FLOAT, "float");
TYPES.put(Type.TYPE_INT32, "int32");
TYPES.put(Type.TYPE_INT64, "int64");
TYPES.put(Type.TYPE_SFIXED32, "sfixed32");
TYPES.put(Type.TYPE_SFIXED64, "sfixed64");
TYPES.put(Type.TYPE_SINT32, "sint32");
TYPES.put(Type.TYPE_SINT64, "sint64");
TYPES.put(Type.TYPE_STRING, "string");
TYPES.put(Type.TYPE_UINT32, "uint32");
TYPES.put(Type.TYPE_UINT64, "uint64");
}
protected final Appendable output;
protected String newline = System.lineSeparator();
protected int indent = 0;
protected int tabWidth = 4;
protected boolean indentWithTabs = false;
protected String absolutePackage = null;
public ProtobufDecompiler(Appendable output) {
this.output = output;
}
public ProtobufDecompiler(OutputStream output) {
this.output = new BufferedWriter(new OutputStreamWriter(output));
}
public static void main(String[] args) throws IOException {
ProtobufDecompiler decompiler = new ProtobufDecompiler((Appendable)System.out);
FileDescriptorSet set;
try (FileInputStream istr = new FileInputStream(args[0])) {
set = FileDescriptorSet.parseFrom(istr);
}
decompiler.decompile(set);
}
public void decompile(FileDescriptorSet setDescriptor) throws IOException {
for (FileDescriptorProto fileDescriptor : setDescriptor.getFileList()) {
newline();
format("===== %s =====", fileDescriptor.getName());
newline();
decompile(fileDescriptor);
}
}
public void decompile(FileDescriptorProto fileDescriptor) throws IOException {
if (fileDescriptor.hasPackage()) {
indentedFormat("package %s;", fileDescriptor.getPackage());
absolutePackage = "." + fileDescriptor.getPackage() + ".";
}
for (String dependency : fileDescriptor.getDependencyList()) {
indentedFormat("import \"%s\";", dependency);
}
if (fileDescriptor.hasOptions()) {
decompileOptions(fileDescriptor.getOptions());
}
decompileMembers(fileDescriptor.getEnumTypeList(),
fileDescriptor.getMessageTypeList(),
Collections.<FieldDescriptorProto>emptyList(),
Collections.<DescriptorProto.ExtensionRange>emptyList(),
fileDescriptor.getExtensionList());
for (ServiceDescriptorProto serviceDescriptor : fileDescriptor.getServiceList()) {
decompile(serviceDescriptor);
}
newline();
flush();
}
protected void decompile(EnumDescriptorProto enumDescriptor) throws IOException {
indentedFormat("enum %s {", enumDescriptor.getName());
indent++;
if (enumDescriptor.hasOptions()) {
decompileOptions(enumDescriptor.getOptions());
}
for (EnumValueDescriptorProto value : enumDescriptor.getValueList()) {
indentedFormat("%s = %d%s;", value.getName(), value.getNumber(),
value.hasOptions() ? bracketedOptions(value.getOptions()) : "");
}
indent--;
indentedFormat("}");
}
protected void decompile(DescriptorProto messageDescriptor) throws IOException {
indentedFormat("message %s {", messageDescriptor.getName());
decompileMessageBody(messageDescriptor);
}
protected void decompile(ServiceDescriptorProto serviceDescriptor) throws IOException {
indentedFormat("service %s {", serviceDescriptor.getName());
indent++;
if (serviceDescriptor.hasOptions()) {
decompileOptions(serviceDescriptor.getOptions());
}
for (MethodDescriptorProto methodDescriptor : serviceDescriptor.getMethodList()) {
indentedFormat("rpc %s (%s) returns (%s)",
methodDescriptor.getName(), methodDescriptor.getInputType(), methodDescriptor.getOutputType());
if (methodDescriptor.hasOptions()) {
write("{ ");
indent++;
decompileOptions(methodDescriptor.getOptions());
indent--;
indentedFormat("}");
}
else {
write(";");
}
}
indent--;
indentedFormat("}");
}
protected void decompileMessageBody(DescriptorProto messageDescriptor) throws IOException {
indent++;
if (messageDescriptor.hasOptions()) {
decompileOptions(messageDescriptor.getOptions());
}
decompileMembers(messageDescriptor.getEnumTypeList(),
messageDescriptor.getNestedTypeList(),
messageDescriptor.getFieldList(),
messageDescriptor.getExtensionRangeList(),
messageDescriptor.getExtensionList());
indent--;
indentedFormat("}");
}
protected void decompileMembers(List<EnumDescriptorProto> enumDescriptors,
List<DescriptorProto> messageDescriptors,
List<FieldDescriptorProto> fieldDescriptors,
List<DescriptorProto.ExtensionRange> extensionRanges,
List<FieldDescriptorProto> extensions)
throws IOException {
for (EnumDescriptorProto enumDescriptor : enumDescriptors) {
decompile(enumDescriptor);
}
Map<String,DescriptorProto> groups = new HashMap<>();
findGroups(fieldDescriptors, groups);
findGroups(extensions, groups);
for (DescriptorProto nestedDescriptor : messageDescriptors) {
if (groups.containsKey(nestedDescriptor.getName())) {
groups.put(nestedDescriptor.getName(), nestedDescriptor);
}
else {
decompile(nestedDescriptor);
}
}
decompileFields(fieldDescriptors, groups);
for (DescriptorProto.ExtensionRange extensionRange : extensionRanges) {
indentedFormat("extensions %s to %s;", extensionRange.getStart(),
(extensionRange.getEnd() == 0x20000000) ? "max" : extensionRange.getEnd());
}
if (!extensions.isEmpty()) {
Map<String,List<FieldDescriptorProto>> extensionsByExtendee = new TreeMap<>();
for (FieldDescriptorProto extension : extensions) {
List<FieldDescriptorProto> entry = extensionsByExtendee.get(extension.getExtendee());
if (entry == null) {
entry = new ArrayList<>();
extensionsByExtendee.put(extension.getExtendee(), entry);
}
entry.add(extension);
}
for (Map.Entry<String,List<FieldDescriptorProto>> entry : extensionsByExtendee.entrySet()) {
indentedFormat("extend %s {", entry.getKey());
indent++;
decompileFields(entry.getValue(), groups);
indent--;
indentedFormat("}");
}
}
}
protected void findGroups(List<FieldDescriptorProto> fieldDescriptors,
Map<String,DescriptorProto> groups) {
for (FieldDescriptorProto fieldDescriptor : fieldDescriptors) {
if (fieldDescriptor.getType() == Type.TYPE_GROUP) {
groups.put(fieldDescriptor.getTypeName(), null);
}
}
}
protected void decompileFields(List<FieldDescriptorProto> fieldDescriptors,
Map<String,DescriptorProto> groups)
throws IOException {
for (FieldDescriptorProto fieldDescriptor : fieldDescriptors) {
String label = LABELS.get(fieldDescriptor.getLabel());
String type = TYPES.get(fieldDescriptor.getType());
String name = fieldDescriptor.getName();
if (fieldDescriptor.hasTypeName()) {
type = fieldDescriptor.getTypeName();
if ((absolutePackage != null) && type.startsWith(absolutePackage)) {
type = type.substring(absolutePackage.length());
}
}
DescriptorProto groupDescriptor = null;
if (fieldDescriptor.getType() == Type.TYPE_GROUP) {
groupDescriptor = groups.get(type);
if (groupDescriptor != null) {
name = type;
type = "group";
}
}
indentedFormat("%s %s %s = %d",
label, type, name, fieldDescriptor.getNumber());
if (fieldDescriptor.hasOptions() || fieldDescriptor.hasDefaultValue()) {
write(defaultAndOptions(fieldDescriptor.hasOptions() ? fieldDescriptor.getOptions() : null,
fieldDescriptor.hasDefaultValue() ? fieldDescriptor.getDefaultValue() : null));
}
if (groupDescriptor == null) {
write(";");
}
else {
decompileMessageBody(groupDescriptor);
}
}
}
protected void decompileOptions(MessageOrBuilder options) throws IOException {
for (Map.Entry<FieldDescriptor,Object> entry : options.getAllFields().entrySet()) {
FieldDescriptor field = entry.getKey();
Object value = entry.getValue();
String fieldName = field.getName();
if (field.isExtension()) {
fieldName = "(" + fieldName + ")";
}
if (field.getType() == FieldDescriptor.Type.MESSAGE) {
for (Map.Entry<FieldDescriptor,Object> subentry : ((MessageOrBuilder)value).getAllFields().entrySet()) {
FieldDescriptor subfield = subentry.getKey();
Object subvalue = subentry.getValue();
indentedFormat("option %s.%s = %s;", fieldName, subfield.getName(), literal(subvalue, subfield.getType()));
}
}
else {
indentedFormat("option %s = %s;", fieldName, literal(value, field.getType()));
}
}
}
protected String bracketedOptions(MessageOrBuilder options) {
return defaultAndOptions(options, null);
}
protected String defaultAndOptions(MessageOrBuilder options, String defaultValue) {
StringBuilder str = new StringBuilder();
boolean first = true;
if (defaultValue != null) {
str.append(" [default = ");
str.append(defaultValue); // TODO: quote
first = false;
}
if (options != null) {
for (Map.Entry<FieldDescriptor,Object> entry : options.getAllFields().entrySet()) {
FieldDescriptor field = entry.getKey();
Object value = entry.getValue();
String fieldName = field.getName();
if (field.isExtension()) {
fieldName = "(" + fieldName + ")";
}
if (field.getType() == FieldDescriptor.Type.MESSAGE) {
for (Map.Entry<FieldDescriptor,Object> subentry : ((MessageOrBuilder)value).getAllFields().entrySet()) {
FieldDescriptor subfield = subentry.getKey();
Object subvalue = subentry.getValue();
if (first) {
str.append(" [");
first = false;
}
else {
str.append(", ");
}
str.append(fieldName).append(".").append(subfield.getName()).append(" = ").append(literal(subvalue, subfield.getType()));
}
}
else {
if (first) {
str.append(" [");
first = false;
}
else {
str.append(", ");
}
str.append(fieldName).append(" = ").append(literal(value, field.getType()));
}
}
}
if (!first) {
str.append("]");
}
return str.toString();
}
protected String literal(Object value, FieldDescriptor.Type type) {
switch (type) {
case STRING:
return quotedString((String)value);
default:
return value.toString();
}
}
protected String quotedString(String str) {
return "\"" + str.replace("\\", "\\\\").replace("\"", "\\\"") + "\"";
}
public String getNewline() {
return newline;
}
public void setNewline(String newline) {
this.newline = newline;
}
public int getTabWidth() {
return tabWidth;
}
public void setTabWidth(int tabWidth) {
this.tabWidth = tabWidth;
}
public boolean isIndentWithTabs() {
return indentWithTabs;
}
public void setIndentWithTabs(boolean indentWithTabs) {
this.indentWithTabs = indentWithTabs;
}
protected void indentedFormat(String fmt, Object... args) throws IOException {
indentedLine();
format(fmt, args);
}
protected void format(String fmt, Object... args) throws IOException {
write(String.format(fmt, args));
}
protected void write(String str) throws IOException {
output.append(str);
}
protected void newline() throws IOException {
write(newline);
}
protected void indentedLine() throws IOException {
newline();
for (int i = 0; i < indent; i++) {
if (indentWithTabs) {
write("\t");
}
else {
for (int j = 0; j < tabWidth; j++) {
write(" ");
}
}
}
}
protected void flush() throws IOException {
if (output instanceof Flushable) {
((Flushable)output).flush();
}
}
}