com.squareup.protoparser.ProtoParser Maven / Gradle / Ivy
// Copyright 2013 Square, Inc.
package com.squareup.protoparser;
import com.google.auto.value.AutoValue;
import com.squareup.protoparser.DataType.MapType;
import com.squareup.protoparser.DataType.NamedType;
import com.squareup.protoparser.DataType.ScalarType;
import java.io.CharArrayWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import static com.squareup.protoparser.ProtoFile.Syntax.PROTO_2;
import static com.squareup.protoparser.ProtoFile.Syntax.PROTO_3;
import static java.nio.charset.StandardCharsets.UTF_8;
/** Basic parser for {@code .proto} schema declarations. */
public final class ProtoParser {
/** Parse a {@code .proto} definition file. */
public static ProtoFile parseUtf8(File file) throws IOException {
try (InputStream is = new FileInputStream(file)) {
return parseUtf8(file.getPath(), is);
}
}
/** Parse a {@code .proto} definition file. */
public static ProtoFile parseUtf8(Path path) throws IOException {
try (Reader reader = Files.newBufferedReader(path, UTF_8)) {
return parse(path.toString(), reader);
}
}
/** Parse a named {@code .proto} schema. The {@code InputStream} is not closed. */
public static ProtoFile parseUtf8(String name, InputStream is) throws IOException {
return parse(name, new InputStreamReader(is, UTF_8));
}
/** Parse a named {@code .proto} schema. The {@code Reader} is not closed. */
public static ProtoFile parse(String name, Reader reader) throws IOException {
CharArrayWriter writer = new CharArrayWriter();
char[] buffer = new char[1024];
int count;
while ((count = reader.read(buffer)) != -1) {
writer.write(buffer, 0, count);
}
return new ProtoParser(name, writer.toCharArray()).readProtoFile();
}
/** Parse a named {@code .proto} schema. */
public static ProtoFile parse(String name, String data) {
return new ProtoParser(name, data.toCharArray()).readProtoFile();
}
private final String filePath;
private final char[] data;
private final ProtoFile.Builder fileBuilder;
/** Our cursor within the document. {@code data[pos]} is the next character to be read. */
private int pos;
/** The number of newline characters encountered thus far. */
private int line;
/** The index of the most recent newline character. */
private int lineStart;
/** Output package name, or null if none yet encountered. */
private String packageName;
/** The current package name + nested type names, separated by dots. */
private String prefix = "";
ProtoParser(String filePath, char[] data) {
this.filePath = filePath;
this.data = data;
this.fileBuilder = ProtoFile.builder(filePath);
}
ProtoFile readProtoFile() {
while (true) {
String documentation = readDocumentation();
if (pos == data.length) {
return fileBuilder.build();
}
Object declaration = readDeclaration(documentation, Context.FILE);
if (declaration instanceof TypeElement) {
fileBuilder.addType((TypeElement) declaration);
} else if (declaration instanceof ServiceElement) {
fileBuilder.addService((ServiceElement) declaration);
} else if (declaration instanceof OptionElement) {
fileBuilder.addOption((OptionElement) declaration);
} else if (declaration instanceof ExtendElement) {
fileBuilder.addExtendDeclaration((ExtendElement) declaration);
}
}
}
private Object readDeclaration(String documentation, Context context) {
// Skip unnecessary semicolons, occasionally used after a nested message declaration.
if (peekChar() == ';') {
pos++;
return null;
}
String label = readWord();
if (label.equals("package")) {
if (!context.permitsPackage()) throw unexpected("'package' in " + context);
if (packageName != null) throw unexpected("too many package names");
packageName = readName();
fileBuilder.packageName(packageName);
prefix = packageName + ".";
if (readChar() != ';') throw unexpected("expected ';'");
return null;
} else if (label.equals("import")) {
if (!context.permitsImport()) throw unexpected("'import' in " + context);
String importString = readString();
if ("public".equals(importString)) {
fileBuilder.addPublicDependency(readString());
} else {
fileBuilder.addDependency(importString);
}
if (readChar() != ';') throw unexpected("expected ';'");
return null;
} else if (label.equals("syntax")) {
if (!context.permitsSyntax()) throw unexpected("'syntax' in " + context);
if (readChar() != '=') throw unexpected("expected '='");
String syntax = readQuotedString();
switch (syntax) {
case "proto2":
fileBuilder.syntax(PROTO_2);
break;
case "proto3":
fileBuilder.syntax(PROTO_3);
break;
default:
throw unexpected("'syntax' must be 'proto2' or 'proto3'. Found: " + syntax);
}
if (readChar() != ';') throw unexpected("expected ';'");
return null;
} else if (label.equals("option")) {
OptionElement result = readOption('=');
if (readChar() != ';') throw unexpected("expected ';'");
return result;
} else if (label.equals("message")) {
return readMessage(documentation);
} else if (label.equals("enum")) {
return readEnumElement(documentation);
} else if (label.equals("service")) {
return readService(documentation);
} else if (label.equals("extend")) {
return readExtend(documentation);
} else if (label.equals("rpc")) {
if (!context.permitsRpc()) throw unexpected("'rpc' in " + context);
return readRpc(documentation);
} else if (label.equals("required") || label.equals("optional") || label.equals("repeated")) {
if (!context.permitsField()) throw unexpected("fields must be nested");
FieldElement.Label labelEnum = FieldElement.Label.valueOf(label.toUpperCase(Locale.US));
return readField(documentation, labelEnum);
} else if (label.equals("oneof")) {
if (!context.permitsOneOf()) throw unexpected("'oneof' must be nested in message");
return readOneOf(documentation);
} else if (label.equals("extensions")) {
if (!context.permitsExtensions()) throw unexpected("'extensions' must be nested");
return readExtensions(documentation);
} else if (context == Context.ENUM) {
if (readChar() != '=') throw unexpected("expected '='");
EnumConstantElement.Builder builder = EnumConstantElement.builder()
.name(label)
.tag(readInt());
if (peekChar() == '[') {
readChar();
while (true) {
builder.addOption(readOption('='));
char c = readChar();
if (c == ']') {
break;
}
if (c != ',') {
throw unexpected("Expected ',' or ']");
}
}
}
if (readChar() != ';') throw unexpected("expected ';'");
documentation = tryAppendTrailingDocumentation(documentation);
return builder.documentation(documentation).build();
} else {
throw unexpected("unexpected label: " + label);
}
}
/** Reads a message declaration. */
private MessageElement readMessage(String documentation) {
String name = readName();
MessageElement.Builder builder = MessageElement.builder()
.name(name)
.qualifiedName(prefix + name)
.documentation(documentation);
String previousPrefix = prefix;
prefix = prefix + name + ".";
if (readChar() != '{') throw unexpected("expected '{'");
while (true) {
String nestedDocumentation = readDocumentation();
if (peekChar() == '}') {
pos++;
break;
}
Object declared = readDeclaration(nestedDocumentation, Context.MESSAGE);
if (declared instanceof FieldElement) {
builder.addField((FieldElement) declared);
} else if (declared instanceof OneOfElement) {
builder.addOneOf((OneOfElement) declared);
} else if (declared instanceof TypeElement) {
builder.addType((TypeElement) declared);
} else if (declared instanceof ExtensionsElement) {
builder.addExtensions((ExtensionsElement) declared);
} else if (declared instanceof OptionElement) {
builder.addOption((OptionElement) declared);
} else if (declared instanceof ExtendElement) {
// Extend declarations always add in a global scope regardless of nesting.
fileBuilder.addExtendDeclaration((ExtendElement) declared);
}
}
prefix = previousPrefix;
return builder.build();
}
/** Reads an extend declaration. */
private ExtendElement readExtend(String documentation) {
String name = readName();
String qualifiedName = name;
if (!name.contains(".") && packageName != null) {
qualifiedName = packageName + "." + name;
}
ExtendElement.Builder builder = ExtendElement.builder()
.name(name)
.qualifiedName(qualifiedName)
.documentation(documentation);
if (readChar() != '{') throw unexpected("expected '{'");
while (true) {
String nestedDocumentation = readDocumentation();
if (peekChar() == '}') {
pos++;
break;
}
Object declared = readDeclaration(nestedDocumentation, Context.EXTEND);
if (declared instanceof FieldElement) {
builder.addField((FieldElement) declared);
}
}
return builder.build();
}
/** Reads a service declaration and returns it. */
private ServiceElement readService(String documentation) {
String name = readName();
ServiceElement.Builder builder = ServiceElement.builder()
.name(name)
.qualifiedName(prefix + name)
.documentation(documentation);
if (readChar() != '{') throw unexpected("expected '{'");
while (true) {
String rpcDocumentation = readDocumentation();
if (peekChar() == '}') {
pos++;
break;
}
Object declared = readDeclaration(rpcDocumentation, Context.SERVICE);
if (declared instanceof RpcElement) {
builder.addRpc((RpcElement) declared);
} else if (declared instanceof OptionElement) {
builder.addOption((OptionElement) declared);
}
}
return builder.build();
}
/** Reads an enumerated type declaration and returns it. */
private EnumElement readEnumElement(String documentation) {
String name = readName();
EnumElement.Builder builder = EnumElement.builder()
.name(name)
.qualifiedName(prefix + name)
.documentation(documentation);
if (readChar() != '{') throw unexpected("expected '{'");
while (true) {
String valueDocumentation = readDocumentation();
if (peekChar() == '}') {
pos++;
break;
}
Object declared = readDeclaration(valueDocumentation, Context.ENUM);
if (declared instanceof EnumConstantElement) {
builder.addConstant((EnumConstantElement) declared);
} else if (declared instanceof OptionElement) {
builder.addOption((OptionElement) declared);
}
}
return builder.build();
}
/** Reads an field declaration and returns it. */
private FieldElement readField(String documentation, FieldElement.Label label) {
DataType type = readDataType();
String name = readName();
if (readChar() != '=') throw unexpected("expected '='");
int tag = readInt();
FieldElement.Builder builder = FieldElement.builder()
.label(label)
.type(type)
.name(name)
.tag(tag);
if (peekChar() == '[') {
pos++;
while (true) {
builder.addOption(readOption('='));
// Check for optional ',' or closing ']'
char c = peekChar();
if (c == ']') {
pos++;
break;
} else if (c == ',') {
pos++;
}
}
}
if (readChar() != ';') {
throw unexpected("expected ';'");
}
documentation = tryAppendTrailingDocumentation(documentation);
return builder.documentation(documentation).build();
}
private OneOfElement readOneOf(String documentation) {
OneOfElement.Builder builder = OneOfElement.builder()
.name(readName())
.documentation(documentation);
if (readChar() != '{') throw unexpected("expected '{'");
while (true) {
String nestedDocumentation = readDocumentation();
if (peekChar() == '}') {
pos++;
break;
}
builder.addField(readField(nestedDocumentation, FieldElement.Label.ONE_OF));
}
return builder.build();
}
/** Reads extensions like "extensions 101;" or "extensions 101 to max;". */
private ExtensionsElement readExtensions(String documentation) {
int start = readInt(); // Range start.
int end = start;
if (peekChar() != ';') {
if (!"to".equals(readWord())) throw unexpected("expected ';' or 'to'");
String s = readWord(); // Range end.
if (s.equals("max")) {
end = ProtoFile.MAX_TAG_VALUE;
} else {
end = Integer.parseInt(s);
}
}
if (readChar() != ';') throw unexpected("expected ';'");
return ExtensionsElement.create(start, end, documentation);
}
/** Reads a option containing a name, an '=' or ':', and a value. */
private OptionElement readOption(char keyValueSeparator) {
boolean isExtension = (peekChar() == '[');
boolean isParenthesized = (peekChar() == '(');
String name = readName(); // Option name.
if (isExtension) {
name = "[" + name + "]";
}
String subName = null;
char c = readChar();
if (c == '.') {
// Read nested field name. For example "baz" in "(foo.bar).baz = 12".
subName = readName();
c = readChar();
}
if (c != keyValueSeparator) {
throw unexpected("expected '" + keyValueSeparator + "' in option");
}
OptionKindAndValue kindAndValue = readKindAndValue();
OptionElement.Kind kind = kindAndValue.kind();
Object value = kindAndValue.value();
if (subName != null) {
value = OptionElement.create(subName, kind, value);
kind = OptionElement.Kind.OPTION;
}
return OptionElement.create(name, kind, value, isParenthesized);
}
@AutoValue
abstract static class OptionKindAndValue {
static OptionKindAndValue of(OptionElement.Kind kind, Object value) {
return new AutoValue_ProtoParser_OptionKindAndValue(kind, value);
}
abstract OptionElement.Kind kind();
abstract Object value();
}
/** Reads a value that can be a map, list, string, number, boolean or enum. */
private OptionKindAndValue readKindAndValue() {
char peeked = peekChar();
switch (peeked) {
case '{':
return OptionKindAndValue.of(OptionElement.Kind.MAP, readMap('{', '}', ':'));
case '[':
return OptionKindAndValue.of(OptionElement.Kind.LIST, readList());
case '"':
return OptionKindAndValue.of(OptionElement.Kind.STRING, readString());
default:
if (Character.isDigit(peeked) || peeked == '-') {
return OptionKindAndValue.of(OptionElement.Kind.NUMBER, readWord());
}
String word = readWord();
switch (word) {
case "true":
return OptionKindAndValue.of(OptionElement.Kind.BOOLEAN, "true");
case "false":
return OptionKindAndValue.of(OptionElement.Kind.BOOLEAN, "false");
default:
return OptionKindAndValue.of(OptionElement.Kind.ENUM, word);
}
}
}
/**
* Returns a map of string keys and values. This is similar to a JSON object,
* with '{' and '}' surrounding the map, ':' separating keys from values, and
* ',' separating entries.
*/
@SuppressWarnings("unchecked")
private Map readMap(char openBrace, char closeBrace, char keyValueSeparator) {
if (readChar() != openBrace) throw new AssertionError();
Map result = new LinkedHashMap<>();
while (true) {
if (peekChar() == closeBrace) {
// If we see the close brace, finish immediately. This handles {}/[] and ,}/,] cases.
pos++;
return result;
}
OptionElement option = readOption(keyValueSeparator);
String name = option.name();
Object value = option.value();
if (value instanceof OptionElement) {
@SuppressWarnings("unchecked")
Map nested = (Map) result.get(name);
if (nested == null) {
nested = new LinkedHashMap<>();
result.put(name, nested);
}
OptionElement valueOption = (OptionElement) value;
nested.put(valueOption.name(), valueOption.value());
} else {
// Add the value(s) to any previous values with the same key
Object previous = result.get(name);
if (previous == null) {
result.put(name, value);
} else if (previous instanceof List) {
// Add to previous List
addToList((List
© 2015 - 2025 Weber Informatics LLC | Privacy Policy