blob: 0f94efa5a393a5c72bacddad73c12b3103574274 [file] [log] [blame]
// Copyright (c) 2016, the R8 project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
package com.android.tools.r8.naming;
import com.android.tools.r8.DiagnosticsHandler;
import com.android.tools.r8.naming.MemberNaming.FieldSignature;
import com.android.tools.r8.naming.MemberNaming.MethodSignature;
import com.android.tools.r8.naming.MemberNaming.Signature;
import com.android.tools.r8.naming.mappinginformation.MappingInformation;
import com.android.tools.r8.naming.mappinginformation.SignatureMappingInformation;
import com.android.tools.r8.position.TextPosition;
import com.android.tools.r8.utils.IdentifierUtils;
import com.android.tools.r8.utils.StringUtils;
import com.google.common.collect.Maps;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import java.io.BufferedReader;
import java.io.IOException;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
/**
* Parses a Proguard mapping file and produces mappings from obfuscated class names to the original
* name and from obfuscated member signatures to the original members the obfuscated member
* was formed of.
* <p>
* The expected format is as follows
* <p>
* original-type-name ARROW obfuscated-type-name COLON starts a class mapping
* description and maps original to obfuscated.
* <p>
* followed by one or more of
* <p>
* signature ARROW name
* <p>
* which maps the member with the given signature to the new name. This mapping is not
* bidirectional as member names are overloaded by signature. To make it bidirectional, we extend
* the name with the signature of the original member.
* <p>
* Due to inlining, we might have the above prefixed with a range (two numbers separated by :).
* <p>
* range COLON signature ARROW name
* <p>
* This has the same meaning as the above but also encodes the line number range of the member. This
* may be followed by multiple inline mappings of the form
* <p>
* range COLON signature COLON range ARROW name
* <p>
* to identify that signature was inlined from the second range to the new line numbers in the first
* range. This is then followed by information on the call trace to where the member was inlined.
* These entries have the form
* <p>
* range COLON signature COLON number ARROW name
* <p>
* and are currently only stored to be able to reproduce them later.
*/
public class ProguardMapReader implements AutoCloseable {
private final BufferedReader reader;
private final JsonParser jsonParser = new JsonParser();
private final DiagnosticsHandler diagnosticsHandler;
private final boolean allowEmptyMappedRanges;
@Override
public void close() throws IOException {
reader.close();
}
ProguardMapReader(
BufferedReader reader,
DiagnosticsHandler diagnosticsHandler,
boolean allowEmptyMappedRanges) {
this.reader = reader;
this.diagnosticsHandler = diagnosticsHandler;
this.allowEmptyMappedRanges = allowEmptyMappedRanges;
assert reader != null;
assert diagnosticsHandler != null;
}
// Internal parser state
private int lineNo = 0;
private int lineOffset = 0;
private String line;
private int peekCodePoint() {
return lineOffset < line.length() ? line.codePointAt(lineOffset) : '\n';
}
private char peekChar(int distance) {
return lineOffset + distance < line.length()
? line.charAt(lineOffset + distance)
: '\n';
}
private boolean hasNext() {
return lineOffset < line.length();
}
private int nextCodePoint() {
try {
int cp = line.codePointAt(lineOffset);
lineOffset += Character.charCount(cp);
return cp;
} catch (ArrayIndexOutOfBoundsException e) {
throw new ParseException("Unexpected end of line");
}
}
private char nextChar() {
assert hasNext();
try {
return line.charAt(lineOffset++);
} catch (ArrayIndexOutOfBoundsException e) {
throw new ParseException("Unexpected end of line");
}
}
private boolean nextLine() throws IOException {
if (line.length() != lineOffset) {
throw new ParseException("Expected end of line");
}
return skipLine();
}
private boolean isEmptyOrCommentLine(String line) {
if (line == null) {
return true;
}
for (int i = 0; i < line.length(); ++i) {
char c = line.charAt(i);
if (c == '#') {
return !hasFirstCharJsonBrace(line, i);
} else if (!StringUtils.isWhitespace(c)) {
return false;
}
}
return true;
}
private boolean isCommentLineWithJsonBrace() {
if (line == null) {
return false;
}
for (int i = 0; i < line.length(); ++i) {
char c = line.charAt(i);
if (c == '#') {
return hasFirstCharJsonBrace(line, i);
} else if (!Character.isWhitespace(c)) {
return false;
}
}
return false;
}
private static boolean hasFirstCharJsonBrace(String line, int commentCharIndex) {
for (int i = commentCharIndex + 1; i < line.length(); i++) {
char c = line.charAt(i);
if (c == '{') {
return true;
} else if (!Character.isWhitespace(c)) {
return false;
}
}
return false;
}
private boolean skipLine() throws IOException {
lineOffset = 0;
do {
lineNo++;
line = reader.readLine();
} while (hasLine() && isEmptyOrCommentLine(line));
return hasLine();
}
private boolean hasLine() {
return line != null;
}
// Helpers for common pattern
private void skipWhitespace() {
while (hasNext() && StringUtils.isWhitespace(peekCodePoint())) {
nextCodePoint();
}
}
private void expectWhitespace() {
boolean seen = false;
while (hasNext() && StringUtils.isWhitespace(peekCodePoint())) {
seen = seen || !StringUtils.isBOM(peekCodePoint());
nextCodePoint();
}
if (!seen) {
throw new ParseException("Expected whitespace", true);
}
}
private void expect(char c) {
if (!hasNext()) {
throw new ParseException("Expected '" + c + "'", true);
}
if (nextChar() != c) {
throw new ParseException("Expected '" + c + "'");
}
}
void parse(ProguardMap.Builder mapBuilder) throws IOException {
// Read the first line.
do {
line = reader.readLine();
lineNo++;
} while (hasLine() && isEmptyOrCommentLine(line));
parseClassMappings(mapBuilder);
}
// Parsing of entries
private void parseClassMappings(ProguardMap.Builder mapBuilder) throws IOException {
while (hasLine()) {
skipWhitespace();
if (isCommentLineWithJsonBrace()) {
// TODO(b/179665169): Parse the mapping information without doing anything with it, since we
// at this point do not have a global context.
MappingInformation.fromJsonObject(parseJsonInComment(), diagnosticsHandler, lineNo);
// Skip reading the rest of the line.
lineOffset = line.length();
nextLine();
}
String before = parseType(false);
skipWhitespace();
// Workaround for proguard map files that contain entries for package-info.java files.
assert IdentifierUtils.isDexIdentifierPart('-');
if (before.endsWith("-") && acceptString(">")) {
// With - as a legal identifier part the grammar is ambiguous, and we treat a->b as a -> b,
// and not as a- > b (which would be a parse error).
before = before.substring(0, before.length() - 1);
} else {
skipWhitespace();
acceptArrow();
}
skipWhitespace();
String after = parseType(false);
skipWhitespace();
expect(':');
ClassNaming.Builder currentClassBuilder =
mapBuilder.classNamingBuilder(after, before, getPosition());
skipWhitespace();
if (nextLine()) {
parseMemberMappings(currentClassBuilder);
}
}
}
private void parseMemberMappings(ClassNaming.Builder classNamingBuilder) throws IOException {
MemberNaming lastAddedNaming = null;
MemberNaming activeMemberNaming = null;
Range previousMappedRange = null;
Map<Signature, SignatureMappingInformation> mappingInformation = Maps.newHashMap();
do {
Object originalRange = null;
Range mappedRange = null;
// Try to parse any information added in comments above member namings
if (isCommentLineWithJsonBrace()) {
MappingInformation mappingInfo =
MappingInformation.fromJsonObject(parseJsonInComment(), diagnosticsHandler, lineNo);
if (mappingInfo != null) {
if (mappingInfo.isSignatureMappingInformation()) {
SignatureMappingInformation sigMapInfo = mappingInfo.asSignatureMappingInformation();
mappingInformation.put(sigMapInfo.getSignature(), sigMapInfo);
} else {
classNamingBuilder.addMappingInformation(mappingInfo, diagnosticsHandler, lineNo);
}
}
// Skip reading the rest of the line.
lineOffset = line.length();
continue;
}
// Parse the member line ' x:y:name:z:q -> renamedName'.
if (!StringUtils.isWhitespace(peekCodePoint())) {
break;
}
skipWhitespace();
Object maybeRangeOrInt = maybeParseRangeOrInt();
if (maybeRangeOrInt != null) {
if (!(maybeRangeOrInt instanceof Range)) {
throw new ParseException(
String.format("Invalid obfuscated line number range (%s).", maybeRangeOrInt));
}
mappedRange = (Range) maybeRangeOrInt;
skipWhitespace();
expect(':');
}
skipWhitespace();
Signature signature = parseSignature();
skipWhitespace();
if (peekChar(0) == ':') {
// This is a mapping or inlining definition
nextChar();
skipWhitespace();
originalRange = maybeParseRangeOrInt();
if (originalRange == null) {
throw new ParseException("No number follows the colon after the method signature.");
}
}
if (!allowEmptyMappedRanges && mappedRange == null && originalRange != null) {
throw new ParseException("No mapping for original range " + originalRange + ".");
}
skipWhitespace();
skipArrow();
skipWhitespace();
String renamedName = parseMethodName();
if (signature instanceof MethodSignature) {
classNamingBuilder.addMappedRange(
mappedRange, (MethodSignature) signature, originalRange, renamedName);
}
assert mappedRange == null || signature instanceof MethodSignature;
// If this line refers to a member that should be added to classNamingBuilder (as opposed to
// an inner inlined callee) and it's different from the the previous activeMemberNaming, then
// flush (add) the current activeMemberNaming.
if (activeMemberNaming != null) {
boolean changedName = !activeMemberNaming.getRenamedName().equals(renamedName);
boolean changedMappedRange = !Objects.equals(previousMappedRange, mappedRange);
if (changedName || previousMappedRange == null || changedMappedRange) {
if (lastAddedNaming == null
|| !lastAddedNaming.getOriginalSignature().equals(activeMemberNaming.signature)) {
classNamingBuilder.addMemberEntry(activeMemberNaming);
lastAddedNaming = activeMemberNaming;
}
}
}
if (mappingInformation.containsKey(signature)) {
activeMemberNaming =
new MemberNaming(
signature,
mappingInformation.get(signature).apply(signature, renamedName, diagnosticsHandler),
getPosition());
} else {
activeMemberNaming =
new MemberNaming(signature, signature.asRenamed(renamedName), getPosition());
}
previousMappedRange = mappedRange;
} while (nextLine());
if (activeMemberNaming != null) {
boolean notAdded =
lastAddedNaming == null
|| !lastAddedNaming.getOriginalSignature().equals(activeMemberNaming.signature);
if (previousMappedRange == null || notAdded) {
classNamingBuilder.addMemberEntry(activeMemberNaming);
}
}
}
private TextPosition getPosition() {
return new TextPosition(0, lineNo, 1);
}
// Parsing of components
private void skipIdentifier(boolean allowInit) {
boolean isInit = false;
if (allowInit && peekChar(0) == '<') {
// swallow the leading < character
nextChar();
isInit = true;
}
// Progard sometimes outputs a ? as a method name. We have tools (dexsplitter) that depends
// on being able to map class names back to the original, but does not care if methods are
// correctly mapped. Using this on proguard output for anything else might not give correct
// remappings.
if (!IdentifierUtils.isDexIdentifierStart(peekCodePoint())
&& !IdentifierUtils.isQuestionMark(peekCodePoint())) {
throw new ParseException("Identifier expected");
}
nextCodePoint();
while (IdentifierUtils.isDexIdentifierPart(peekCodePoint())
|| IdentifierUtils.isQuestionMark(peekCodePoint())) {
nextCodePoint();
}
if (isInit) {
expect('>');
}
if (IdentifierUtils.isDexIdentifierPart(peekCodePoint())) {
throw new ParseException(
"End of identifier expected (was 0x" + Integer.toHexString(peekCodePoint()) + ")");
}
}
// Cache for canonicalizing strings.
// This saves 10% of heap space for large programs.
final HashMap<String, String> cache = new HashMap<>();
private String substring(int start) {
String result = line.substring(start, lineOffset);
if (cache.containsKey(result)) {
return cache.get(result);
}
cache.put(result, result);
return result;
}
private String parseMethodName() {
int startPosition = lineOffset;
skipIdentifier(true);
while (peekChar(0) == '.') {
nextChar();
skipIdentifier(true);
}
return substring(startPosition);
}
private String parseType(boolean allowArray) {
int startPosition = lineOffset;
skipIdentifier(false);
while (peekChar(0) == '.') {
nextChar();
skipIdentifier(false);
}
if (allowArray) {
while (peekChar(0) == '[') {
nextChar();
expect(']');
}
}
return substring(startPosition);
}
private Signature parseSignature() {
String type = parseType(true);
expectWhitespace();
String name = parseMethodName();
skipWhitespace();
Signature signature;
if (peekChar(0) == '(') {
nextChar();
skipWhitespace();
String[] arguments;
if (peekChar(0) == ')') {
arguments = new String[0];
} else {
List<String> items = new LinkedList<>();
items.add(parseType(true));
skipWhitespace();
while (peekChar(0) != ')') {
skipWhitespace();
expect(',');
skipWhitespace();
items.add(parseType(true));
}
arguments = items.toArray(StringUtils.EMPTY_ARRAY);
}
expect(')');
signature = new MethodSignature(name, type, arguments);
} else {
signature = new FieldSignature(name, type);
}
return signature;
}
private void skipArrow() {
expect('-');
expect('>');
}
private boolean acceptArrow() {
if (peekChar(0) == '-' && peekChar(1) == '>') {
nextChar();
nextChar();
return true;
}
return false;
}
private boolean acceptString(String s) {
for (int i = 0; i < s.length(); i++) {
if (peekChar(i) != s.charAt(i)) {
return false;
}
}
for (int i = 0; i < s.length(); i++) {
nextChar();
}
return true;
}
private boolean isSimpleDigit(char c) {
return '0' <= c && c <= '9';
}
private Object maybeParseRangeOrInt() {
if (!isSimpleDigit(peekChar(0))) {
return null;
}
int from = parseNumber();
skipWhitespace();
if (peekChar(0) != ':') {
return from;
}
expect(':');
skipWhitespace();
int to = parseNumber();
return new Range(from, to);
}
private int parseNumber() {
int result = 0;
if (!isSimpleDigit(peekChar(0))) {
throw new ParseException("Number expected");
}
do {
result *= 10;
result += Character.getNumericValue(nextChar());
} while (isSimpleDigit(peekChar(0)));
return result;
}
private JsonObject parseJsonInComment() {
assert isCommentLineWithJsonBrace();
try {
int firstIndex = 0;
while (line.charAt(firstIndex) != '{') {
firstIndex++;
}
return jsonParser.parse(line.substring(firstIndex)).getAsJsonObject();
} catch (com.google.gson.JsonSyntaxException ex) {
// An info message is reported in MappingInformation.
return null;
}
}
public class ParseException extends RuntimeException {
private final int lineNo;
private final int lineOffset;
private final boolean eol;
private final String msg;
ParseException(String msg) {
this(msg, false);
}
ParseException(String msg, boolean eol) {
lineNo = ProguardMapReader.this.lineNo;
lineOffset = ProguardMapReader.this.lineOffset;
this.eol = eol;
this.msg = msg;
}
@Override
public String toString() {
if (eol) {
return "Parse error [" + lineNo + ":eol] " + msg;
} else {
return "Parse error [" + lineNo + ":" + lineOffset + "] " + msg;
}
}
}
}