// 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.logging.Log;
import com.android.tools.r8.naming.MemberNaming.FieldSignature;
import com.android.tools.r8.naming.MemberNaming.MethodSignature;
import com.android.tools.r8.naming.MemberNaming.Range;
import com.android.tools.r8.naming.MemberNaming.Signature;
import com.android.tools.r8.naming.MemberNaming.SingleLineRange;
import com.google.common.collect.ImmutableMap;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
/**
* 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.
*
* The expected format is as follows
*
* original-type-name ARROW obfuscated-type-name COLON starts a class mapping
* description and maps original to obfuscated.
*
* followed by one or more of
*
* signature ARROW name
*
* 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.
*
* Due to inlining, we might have the above prefixed with a range (two numbers separated by :).
*
* range COLON signature ARROW name
*
* 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
*
* range COLON signature COLON range ARROW name
*
* 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
*
* range COLON signature COLON number ARROW name
*
* and are currently only stored to be able to reproduce them later.
*/
public class ProguardMapReader implements AutoCloseable {
private final BufferedReader reader;
public void close() throws IOException {
if (reader != null) {
reader.close();
}
}
private ProguardMapReader(BufferedReader reader) throws IOException {
this.reader = reader;
}
public static ClassNameMapper mapperFromInputStream(InputStream in) throws IOException {
BufferedReader reader = new BufferedReader(new InputStreamReader(in, "UTF8"));
try (ProguardMapReader proguardReader = new ProguardMapReader(reader)) {
return proguardReader.parse();
}
}
public static ClassNameMapper mapperFromFile(Path path) throws IOException {
return mapperFromInputStream(Files.newInputStream(path));
}
public static ClassNameMapper mapperFromString(String contents) throws IOException {
return mapperFromInputStream(
new ByteArrayInputStream(contents.getBytes(StandardCharsets.UTF_8)));
}
// Internal parser state
private int lineNo = 0;
private int lineOffset = 0;
private String line;
private char peek() {
return peek(0);
}
private char peek(int distance) {
return lineOffset + distance < line.length()
? line.charAt(lineOffset + distance)
: '\n';
}
private char next() {
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 skipLine() throws IOException {
lineNo++;
lineOffset = 0;
line = reader.readLine();
return hasLine();
}
private boolean hasLine() {
return line != null;
}
// Helpers for common pattern
private void skipWhitespace() {
while (Character.isWhitespace(peek())) {
next();
}
}
private char expect(char c) {
if (next() != c) {
throw new ParseException("Expected '" + c + "'");
}
return c;
}
public ClassNameMapper parse() throws IOException {
// Read the first line.
line = reader.readLine();
Map classNames = parseClassMappings();
return new ClassNameMapper(classNames);
}
// Parsing of entries
private Map parseClassMappings() throws IOException {
ImmutableMap.Builder builder = ImmutableMap.builder();
while (hasLine()) {
String before = parseType(false);
skipWhitespace();
// Workaround for proguard map files that contain entries for package-info.java files.
if (!acceptArrow()) {
// If this was a package-info line, we parsed the "package" string.
if (!before.endsWith("package") || !acceptString("-info")) {
throw new ParseException("Expected arrow after class name " + before);
}
skipLine();
continue;
}
skipWhitespace();
String after = parseType(false);
expect(':');
ClassNaming currentClass = new ClassNaming(after, before);
builder.put(after, currentClass);
if (nextLine()) {
parseMemberMappings(currentClass);
}
}
return builder.build();
}
private void parseMemberMappings(ClassNaming currentClass) throws IOException {
MemberNaming current = null;
Range previousInlineRange = null;
Signature previousSignature = null;
String previousRenamedName = null;
List> collectedInfos = new ArrayList<>(10);
while (Character.isWhitespace(peek())) {
skipWhitespace();
Range inlinedLineRange = maybeParseRange();
if (inlinedLineRange != null) {
expect(':');
}
Signature signature = parseSignature();
Range originalLineRange;
if (peek() == ':') {
// This is an inlining definition
next();
originalLineRange = maybeParseRange();
if (originalLineRange == null) {
if (!skipLine()) {
break;
}
continue;
}
} else {
originalLineRange = null;
}
skipWhitespace();
skipArrow();
skipWhitespace();
String renamedName = parseMethodName();
// If there is no line number information at the front or if it changes, we have a new
// segment. Likewise, if the range information on the right hand side has two values, we have
// a new segment.
if (inlinedLineRange == null
|| previousInlineRange == null
|| originalLineRange == null
|| !previousInlineRange.equals(inlinedLineRange)
|| !originalLineRange.isSingle()) {
// We are at a range boundary. Either we parsed something new, or an inline frame is over.
// We detect this by checking whether the previous signature matches the one of current.
if (current == null || !previousSignature.equals(current.signature)) {
if (collectedInfos.size() == 1) {
current = new MemberNaming(previousSignature, previousRenamedName, previousInlineRange);
currentClass.addMemberEntry(current);
} else {
if (Log.ENABLED && !collectedInfos.isEmpty()) {
Log.warn(getClass(),
"More than one member entry that forms a new group at %s %s -> %s",
previousInlineRange, previousSignature, previousRenamedName);
}
}
} else {
MemberNaming finalCurrent = current;
collectedInfos.forEach(info -> info.accept(finalCurrent));
}
collectedInfos.clear();
}
// Defer the creation of the info until we have the correct member.
collectedInfos.add((m) -> m.addInliningRange(inlinedLineRange, signature, originalLineRange));
// We have parsed the whole line, move on.
previousInlineRange = inlinedLineRange;
previousSignature = signature;
previousRenamedName = renamedName;
if (!nextLine()) {
break;
}
}
// Process the last round if lines have been read.
if (current == null || !previousSignature.equals(current.signature)) {
if (collectedInfos.size() == 1) {
current = new MemberNaming(previousSignature, previousRenamedName, previousInlineRange);
currentClass.addMemberEntry(current);
}
} else {
MemberNaming finalCurrent = current;
collectedInfos.forEach(info -> info.accept(finalCurrent));
}
collectedInfos.clear();
}
// Parsing of components
private void skipIdentifier(boolean allowInit) {
boolean isInit = false;
if (allowInit && peek() == '<') {
// swallow the leading < character
next();
isInit = true;
}
if (!Character.isJavaIdentifierStart(peek())) {
throw new ParseException("Identifier expected");
}
next();
while (Character.isJavaIdentifierPart(peek())) {
next();
}
if (isInit) {
expect('>');
}
if (Character.isJavaIdentifierPart(peek())) {
throw new ParseException("End of identifier expected");
}
}
// Cache for canonicalizing strings.
// This saves 10% of heap space for large programs.
final HashMap 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 (peek() == '.') {
next();
skipIdentifier(true);
}
return substring(startPosition);
}
private String parseType(boolean allowArray) {
int startPosition = lineOffset;
skipIdentifier(false);
while (peek() == '.') {
next();
skipIdentifier(false);
}
if (allowArray) {
while (peek() == '[') {
next();
expect(']');
}
}
return substring(startPosition);
}
private Signature parseSignature() {
String type = parseType(true);
expect(' ');
String name = parseMethodName();
Signature signature;
if (peek() == '(') {
next();
String[] arguments;
if (peek() == ')') {
arguments = new String[0];
} else {
List items = new LinkedList<>();
items.add(parseType(true));
while (peek() != ')') {
expect(',');
items.add(parseType(true));
}
arguments = items.toArray(new String[items.size()]);
}
expect(')');
signature = new MethodSignature(name, type, arguments);
} else {
signature = new FieldSignature(name, type);
}
return signature;
}
private void skipArrow() {
expect('-');
expect('>');
}
private boolean acceptArrow() {
if (peek() == '-' && peek(1) == '>') {
next();
next();
return true;
}
return false;
}
private boolean acceptString(String s) {
for (int i = 0; i < s.length(); i++) {
if (peek(i) != s.charAt(i)) {
return false;
}
}
for (int i = 0; i < s.length(); i++) {
next();
}
return true;
}
private Range maybeParseRange() {
if (!Character.isDigit(peek())) {
return null;
}
int from = parseNumber();
if (peek() != ':') {
return new SingleLineRange(from);
}
expect(':');
int to = parseNumber();
return new Range(from, to);
}
private int parseNumber() {
int result = 0;
if (!Character.isDigit(peek())) {
throw new ParseException("Number expected");
}
do {
result *= 10;
result += Character.getNumericValue(next());
} while (Character.isDigit(peek()));
return result;
}
private class ParseException extends RuntimeException {
private final int lineNo;
private final int lineOffset;
private final String msg;
ParseException(String msg) {
lineNo = ProguardMapReader.this.lineNo;
lineOffset = ProguardMapReader.this.lineOffset;
this.msg = msg;
}
public String toString() {
return "Parse error [" + lineNo + ":" + lineOffset + "] " + msg;
}
}
}