blob: b8257f3ba3240db77d176681f853daa725e65709 [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.ClassNamingForNameMapper.MappedRange;
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.PositionRangeAllocator.CardinalPositionRangeAllocator;
import com.android.tools.r8.naming.PositionRangeAllocator.NonCardinalPositionRangeAllocator;
import com.android.tools.r8.naming.mappinginformation.MapVersionMappingInformation;
import com.android.tools.r8.naming.mappinginformation.MappingInformation;
import com.android.tools.r8.naming.mappinginformation.MappingInformation.ReferentialMappingInformation;
import com.android.tools.r8.naming.mappinginformation.MappingInformationDiagnostics;
import com.android.tools.r8.naming.mappinginformation.ResidualSignatureMappingInformation;
import com.android.tools.r8.naming.mappinginformation.ResidualSignatureMappingInformation.ResidualFieldSignatureMappingInformation;
import com.android.tools.r8.naming.mappinginformation.ResidualSignatureMappingInformation.ResidualMethodSignatureMappingInformation;
import com.android.tools.r8.position.Position;
import com.android.tools.r8.utils.ArrayUtils;
import com.android.tools.r8.utils.BooleanBox;
import com.android.tools.r8.utils.Box;
import com.android.tools.r8.utils.DescriptorUtils;
import com.android.tools.r8.utils.IdentifierUtils;
import com.android.tools.r8.utils.StringUtils;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Objects;
import java.util.function.Consumer;
import java.util.function.Function;
/**
* 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 LineReader reader;
private final JsonParser jsonParser = new JsonParser();
private final DiagnosticsHandler diagnosticsHandler;
private final boolean allowEmptyMappedRanges;
private final boolean allowExperimentalMapping;
private boolean seenClassMapping = false;
private final CardinalPositionRangeAllocator cardinalRangeCache =
PositionRangeAllocator.createCardinalPositionRangeAllocator();
private final NonCardinalPositionRangeAllocator nonCardinalRangeCache =
PositionRangeAllocator.createNonCardinalPositionRangeAllocator();
@Override
public void close() throws IOException {
reader.close();
}
ProguardMapReader(
LineReader reader,
DiagnosticsHandler diagnosticsHandler,
boolean allowEmptyMappedRanges,
boolean allowExperimentalMapping) {
this(
reader,
diagnosticsHandler,
allowEmptyMappedRanges,
allowExperimentalMapping,
MapVersion.MAP_VERSION_NONE);
}
ProguardMapReader(
LineReader reader,
DiagnosticsHandler diagnosticsHandler,
boolean allowEmptyMappedRanges,
boolean allowExperimentalMapping,
MapVersion mapVersion) {
this.reader = reader;
this.diagnosticsHandler = diagnosticsHandler;
this.allowEmptyMappedRanges = allowEmptyMappedRanges;
this.allowExperimentalMapping = allowExperimentalMapping;
this.version = mapVersion;
assert reader != null;
assert diagnosticsHandler != null;
}
// Internal parser state
private int lineNo = 0;
private int lineOffset = 0;
private String line;
private MapVersion version;
private int peekCodePoint() {
return peekCodePoint(0);
}
private int peekCodePoint(int distance) {
return lineOffset + distance < line.length() ? line.codePointAt(lineOffset + distance) : '\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(ProguardMap.Builder mapBuilder) throws IOException {
if (line.length() != lineOffset) {
throw new ParseException("Expected end of line");
}
return skipLine(mapBuilder);
}
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(ProguardMap.Builder mapBuilder) throws IOException {
lineOffset = 0;
boolean isEmptyOrCommentLine;
do {
line = reader.readLine();
lineNo++;
isEmptyOrCommentLine = isEmptyOrCommentLine(line);
if (!seenClassMapping && isEmptyOrCommentLine) {
mapBuilder.addPreambleLine(line);
}
} while (hasLine() && isEmptyOrCommentLine);
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.
skipLine(mapBuilder);
parseClassMappings(mapBuilder);
}
// Parsing of entries
private void parseClassMappings(ProguardMap.Builder mapBuilder) throws IOException {
while (hasLine()) {
skipWhitespace();
if (isCommentLineWithJsonBrace()) {
if (!parseMappingInformation(
info -> {
assert info.isMapVersionMappingInformation()
|| info.isUnknownJsonMappingInformation()
|| info.isPartitionFileNameInformation();
if (info.isMapVersionMappingInformation()) {
mapBuilder.setCurrentMapVersion(info.asMapVersionMappingInformation());
} else if (info.isPartitionFileNameInformation()) {
info.asPartitionFileNameInformation()
.getTypeNameToFileNameMapping()
.forEach(mapBuilder::addFileName);
} else if (!seenClassMapping) {
mapBuilder.addPreambleLine(line);
}
})) {
if (!seenClassMapping) {
mapBuilder.addPreambleLine(line);
}
}
// Skip reading the rest of the line.
lineOffset = line.length();
nextLine(mapBuilder);
continue;
}
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(':');
seenClassMapping = true;
ClassNaming.Builder currentClassBuilder =
mapBuilder.classNamingBuilder(after, before, getPosition());
skipWhitespace();
if (nextLine(mapBuilder)) {
parseMemberMappings(mapBuilder, currentClassBuilder);
}
}
}
private boolean parseMappingInformation(Consumer<MappingInformation> onMappingInfo) {
JsonObject object = parseJsonInComment();
if (object != null) {
MappingInformation.fromJsonObject(
version,
object,
diagnosticsHandler,
lineNo,
info -> {
MapVersionMappingInformation generatorInfo = info.asMapVersionMappingInformation();
if (generatorInfo != null) {
if (generatorInfo.getMapVersion().equals(MapVersion.MAP_VERSION_EXPERIMENTAL)) {
// A mapping file that is marked "experimental" will be treated as an unversioned
// file if the compiler/tool is not explicitly running with experimental support.
version =
allowExperimentalMapping
? MapVersion.MAP_VERSION_EXPERIMENTAL
: MapVersion.MAP_VERSION_NONE;
} else {
version = generatorInfo.getMapVersion();
}
}
onMappingInfo.accept(info);
});
return true;
}
return false;
}
@SuppressWarnings("ReferenceEquality")
private void parseMemberMappings(
ProguardMap.Builder mapBuilder, ClassNaming.Builder classNamingBuilder) throws IOException {
MemberNaming lastAddedNaming = null;
// To ensure we only commit a member if we have the residual signature, we delay creating the
// object and have the variables out here.
Signature previousOriginalSignature = null;
String previousRenamedName = null;
int previousLineNumber = -1;
Range previousRange = null;
Box<Signature> currentResidualSignature = new Box<>();
Box<List<ReferentialMappingInformation>> currentMappingInfoForMemberNaming = new Box<>();
MappedRange activeMappedRange = null;
do {
Range originalRange = null;
// Try to parse any information added in comments above member namings
if (isCommentLineWithJsonBrace()) {
final String currentRenamedNameFinal = previousRenamedName;
final MappedRange currentRange = activeMappedRange;
final MemberNaming lastAddedNamingFinal = lastAddedNaming;
// Reading global info should cause member mapping to return since we are now reading
// headers pertaining to what could be a concatinated file.
BooleanBox readGlobalInfo = new BooleanBox(false);
parseMappingInformation(
info -> {
readGlobalInfo.set(info.isGlobalMappingInformation());
if (currentRenamedNameFinal == null) {
classNamingBuilder.addMappingInformation(
info,
conflictingInfo ->
diagnosticsHandler.warning(
MappingInformationDiagnostics.notAllowedCombination(
info, conflictingInfo, lineNo)));
return;
}
// Always add information to the current range, this will ensure that additional
// information is always placed the same place.
if (currentRange != null) {
currentRange.addMappingInformation(
info,
conflictingInfo ->
diagnosticsHandler.warning(
MappingInformationDiagnostics.notAllowedCombination(
info, conflictingInfo, lineNo)));
}
if (info.isReferentialMappingInformation()) {
ReferentialMappingInformation referentialMappingInformation =
info.asReferentialMappingInformation();
MappingInformation.addMappingInformation(
currentMappingInfoForMemberNaming.computeIfAbsent(ArrayList::new),
referentialMappingInformation,
conflictingInfo ->
diagnosticsHandler.warning(
MappingInformationDiagnostics.notAllowedCombination(
info, conflictingInfo, lineNo)));
if (info.isResidualSignatureMappingInformation()) {
ResidualSignatureMappingInformation mappingInfo =
info.asResidualSignatureMappingInformation();
if (!mappingInfo.isValid()) {
diagnosticsHandler.warning(
MappingInformationDiagnostics.invalidResidualSignature(
line.trim(), lineNo));
return;
}
Signature residualSignature =
getResidualSignatureFromMappingInformation(
info.asResidualSignatureMappingInformation(), currentRenamedNameFinal);
currentResidualSignature.set(residualSignature);
if (currentRange != null) {
if (!mappingInfo.isResidualMethodSignatureMappingInformation()) {
diagnosticsHandler.warning(
MappingInformationDiagnostics.invalidResidualSignatureType(
info.serialize(), lineNo));
currentResidualSignature.clear();
return;
}
if (!isMappedRangeLastAddedNaming(lastAddedNamingFinal, currentRange)) {
currentRange.setResidualSignatureInternal(
residualSignature.asMethodSignature());
}
}
}
}
});
if (readGlobalInfo.isTrue()) {
break;
}
// 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();
Range mappedRange = parseRange(true);
if (mappedRange != null) {
if (mappedRange.isCardinal) {
throw new ParseException(
String.format("Invalid obfuscated line number range (%s).", mappedRange));
}
skipWhitespace();
expect(':');
}
skipWhitespace();
Signature signature = parseSignature();
skipWhitespace();
if (peekChar(0) == ':') {
// This is a mapping or inlining definition
nextChar();
skipWhitespace();
originalRange = parseRange(false);
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.isMethodSignature()) {
MethodSignature residualSignature = null;
// Propagate the active residual signature down to the next read mapped range if the
// original signature and name is the same - that is, there are no inline frames.
if (activeMappedRange != null
&& activeMappedRange.signature == signature
&& activeMappedRange.renamedName.equals(renamedName)) {
residualSignature = activeMappedRange.getResidualSignature();
}
activeMappedRange =
classNamingBuilder.addMappedRange(
mappedRange, signature.asMethodSignature(), originalRange, renamedName);
if (activeMappedRange != null) {
if (residualSignature != null) {
activeMappedRange.setResidualSignatureInternal(residualSignature);
} else if (isMappedRangeLastAddedNaming(lastAddedNaming, activeMappedRange)) {
// If we already parsed a residual signature for the newly read mapped range and have
// lost the information about the residual signature we re-create it again.
activeMappedRange.setResidualSignatureInternal(
lastAddedNaming.getResidualSignature().asMethodSignature());
}
}
}
assert mappedRange == null || signature.isMethodSignature();
if (previousOriginalSignature != null) {
boolean changedName = !previousRenamedName.equals(renamedName);
boolean changedMappedRange =
previousRange == null || !Objects.equals(previousRange, mappedRange);
boolean originalRangeChange = originalRange == null || !originalRange.isCardinal;
if (changedName || changedMappedRange || originalRangeChange) {
lastAddedNaming =
addMemberEntryOrCopyInformation(
lastAddedNaming,
previousOriginalSignature,
previousRenamedName,
previousLineNumber,
currentResidualSignature,
currentMappingInfoForMemberNaming,
previousRange,
classNamingBuilder);
}
}
previousOriginalSignature = signature;
previousRenamedName = renamedName;
previousLineNumber = lineNo;
previousRange = mappedRange;
} while (nextLine(mapBuilder));
if (previousOriginalSignature != null) {
addMemberEntryOrCopyInformation(
lastAddedNaming,
previousOriginalSignature,
previousRenamedName,
previousLineNumber,
currentResidualSignature,
currentMappingInfoForMemberNaming,
previousRange,
classNamingBuilder);
}
}
private boolean isMappedRangeLastAddedNaming(
MemberNaming lastAddedNaming, MappedRange activeMappedRange) {
return lastAddedNaming != null
&& lastAddedNaming.getOriginalSignature().equals(activeMappedRange.getOriginalSignature());
}
private MemberNaming addMemberEntryOrCopyInformation(
MemberNaming lastAddedNaming,
Signature originalSignature,
String renamedName,
int lineNumber,
Box<Signature> residualSignature,
Box<List<ReferentialMappingInformation>> additionalMappingInformation,
Range previousMappedRange,
ClassNaming.Builder classNamingBuilder) {
// 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 (previousMappedRange == null
|| lastAddedNaming == null
|| !lastAddedNaming.getRenamedName().equals(renamedName)
|| !lastAddedNaming.getOriginalSignature().equals(originalSignature)) {
Signature lookupKey =
getResidualSignatureForMemberNaming(residualSignature, originalSignature, renamedName);
MemberNaming newMemberNaming = classNamingBuilder.lookupMemberEntry(lookupKey);
if (newMemberNaming == null) {
newMemberNaming =
new MemberNaming(originalSignature, lookupKey, new LinePosition(lineNumber));
}
if (additionalMappingInformation.isSet()) {
newMemberNaming.addAllMappingInformation(additionalMappingInformation.get());
}
classNamingBuilder.addMemberEntry(newMemberNaming);
residualSignature.clear();
additionalMappingInformation.clear();
return newMemberNaming;
}
if (additionalMappingInformation.isSet()) {
lastAddedNaming.addAllMappingInformation(additionalMappingInformation.get());
additionalMappingInformation.clear();
}
residualSignature.clear();
return lastAddedNaming;
}
private Signature getResidualSignatureFromMappingInformation(
ResidualSignatureMappingInformation mappingInformation, String renamedName) {
if (mappingInformation.isResidualMethodSignatureMappingInformation()) {
ResidualMethodSignatureMappingInformation residualSignatureInfo =
mappingInformation.asResidualMethodSignatureMappingInformation();
MethodSignature residualSignature =
new MethodSignature(
renamedName,
DescriptorUtils.descriptorToJavaType(residualSignatureInfo.getReturnType()),
ArrayUtils.mapToStringArray(
residualSignatureInfo.getParameters(), DescriptorUtils::descriptorToJavaType));
return signatureCache
.computeIfAbsent(residualSignature, Function.identity())
.asMethodSignature();
} else {
assert mappingInformation.isResidualFieldSignatureMappingInformation();
ResidualFieldSignatureMappingInformation residualSignatureInfo =
mappingInformation.asResidualFieldSignatureMappingInformation();
FieldSignature residualSignature =
new FieldSignature(
renamedName, DescriptorUtils.descriptorToJavaType(residualSignatureInfo.getType()));
return signatureCache
.computeIfAbsent(residualSignature, Function.identity())
.asFieldSignature();
}
}
private Signature getResidualSignatureForMemberNaming(
Box<Signature> residualSignature, Signature originalSignature, String renamedName) {
if (residualSignature.isSet()) {
if (residualSignature.get().kind() != originalSignature.kind()) {
diagnosticsHandler.warning(
MappingInformationDiagnostics.invalidResidualSignatureType(
residualSignature.get().toString(), lineNo));
} else {
return residualSignature.get();
}
}
Signature signature = originalSignature.asRenamed(renamedName);
signature = signatureCache.computeIfAbsent(signature, Function.identity());
return signature;
}
private Position getPosition() {
return new LinePosition(lineNo);
}
private static final class LinePosition implements Position {
private final int lineNo;
LinePosition(int lineNo) {
this.lineNo = lineNo;
}
@Override
public String getDescription() {
return "line " + lineNo;
}
@Override
public int hashCode() {
return lineNo;
}
@Override
public boolean equals(Object o) {
if (o == this) {
return true;
}
if (o instanceof LinePosition) {
return lineNo == ((LinePosition) o).lineNo;
}
return false;
}
}
// Parsing of components
private static boolean isAllowedIdentifierStart(int codePoint) {
if (IdentifierUtils.isDexIdentifierStart(codePoint)) {
return true;
}
// Proguard 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.isQuestionMark(codePoint)) {
return true;
}
// Some mapping files contain entries starting with a '.', allow those for compatibility.
if (codePoint == '.') {
return true;
}
return false;
}
private void skipIdentifier(boolean allowInit) {
boolean isInit = false;
if (allowInit && peekChar(0) == '<') {
// swallow the leading < character
nextChar();
isInit = true;
}
if (!isAllowedIdentifierStart(peekCodePoint())) {
throw new ParseException("Identifier expected");
}
nextCodePoint();
while (IdentifierUtils.isDexIdentifierPart(peekCodePoint())
|| IdentifierUtils.isQuestionMark(peekCodePoint())) {
nextCodePoint();
}
if (isInit) {
expect('>');
// There are some files that include identifier text after <clinit>, such as
// <clinit>$redex$opt as seen in b/309080420.
while (IdentifierUtils.isDexIdentifierPart(peekCodePoint())) {
nextCodePoint();
}
}
if (IdentifierUtils.isDexIdentifierPart(peekCodePoint())) {
throw new ParseException(
"End of identifier expected (was 0x" + Integer.toHexString(peekCodePoint()) + ")");
}
}
// Small direct-mapped cache for computing String.substring.
//
// Due to inlining, the same method names and parameter types will occur repeatedly on multiple
// lines. String.substring ends up allocating a lot of garbage, so we use this cache to find
// String objects without having to allocate memory.
//
// "Direct-mapped" is inspired from computer architecture, where having a lookup policy in
// which entries can only ever map to one cache line is often faster than a fancy LRU cache.
private static final int SUBSTRING_CACHE_SIZE = 64;
private final String[] substringCache = new String[SUBSTRING_CACHE_SIZE];
// Cache for canonicalizing strings.
// This saves 10% of heap space for large programs.
private final HashMap<String, String> identifierCache = new HashMap<>();
// Cache for canonicalizing signatures.
//
// Due to inlining, the same MethodSignature will come up many times in a ProguardMap.
// This happens to help a bit for FieldSignature too, so lump those in.
private final HashMap<Signature, Signature> signatureCache = new HashMap<>();
private String substring(int start) {
int cacheIdx;
{
// Check if there was a recent String accessed which matches the substring.
int len = lineOffset - start;
cacheIdx = len % SUBSTRING_CACHE_SIZE;
String candidate = substringCache[cacheIdx];
if (candidate != null
&& candidate.length() == len
&& line.regionMatches(start, candidate, 0, len)) {
return candidate;
}
}
String result = line.substring(start, lineOffset);
return substringCache[cacheIdx] = identifierCache.computeIfAbsent(result, Function.identity());
}
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 = StringUtils.EMPTY_ARRAY;
} else {
List<String> items = new ArrayList<>();
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 signatureCache.computeIfAbsent(signature, Function.identity());
}
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 Range parseRange(boolean colonTerminated) {
if (!isSimpleDigit(peekChar(0))) {
return null;
}
// A colon-terminated range must be of the form "<digit>+<whitespace>*<colon>". If it is not,
// then we must not consume any of the text as it may then be part of an identifier.
if (colonTerminated) {
int i = 1;
while (isSimpleDigit(peekChar(i))) {
i++;
}
while (StringUtils.isWhitespace(peekCodePoint(i))) {
i++;
}
if (peekChar(i) != ':') {
return null;
}
}
int from = parseNumber();
skipWhitespace();
if (peekChar(0) != ':') {
return cardinalRangeCache.get(from);
}
expect(':');
skipWhitespace();
int to = parseNumber();
if (from > to) {
// Past versions of R8 would incorrectly put 0 as the "to" range in some instances
// and fail to order the range. For 0-values assume the range is a singleton position.
if (to == 0) {
to = from;
} else {
int tmp = to;
to = from;
from = tmp;
}
}
return nonCardinalRangeCache.get(from, to);
}
@SuppressWarnings("CharacterGetNumericValue")
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) {
super(msg);
lineNo = ProguardMapReader.this.lineNo;
lineOffset = ProguardMapReader.this.lineOffset;
this.eol = eol;
this.msg = msg;
}
@Override
public String getMessage() {
if (eol) {
return "Parse error [" + lineNo + ":eol] " + msg;
} else {
return "Parse error [" + lineNo + ":" + lineOffset + "] " + msg;
}
}
}
}