|  | // 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 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(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(); | 
|  | if (info.isMapVersionMappingInformation()) { | 
|  | mapBuilder.setCurrentMapVersion(info.asMapVersionMappingInformation()); | 
|  | } 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; | 
|  | } | 
|  |  | 
|  | 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; | 
|  | // 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; | 
|  | } | 
|  | 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(); | 
|  | 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(); | 
|  | 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 (lastAddedNaming != null | 
|  | && lastAddedNaming | 
|  | .getOriginalSignature() | 
|  | .equals(activeMappedRange.getOriginalSignature())) { | 
|  | // 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 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 void skipIdentifier(boolean allowInit) { | 
|  | boolean isInit = false; | 
|  | if (allowInit && peekChar(0) == '<') { | 
|  | // swallow the leading < character | 
|  | nextChar(); | 
|  | isInit = 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.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()) + ")"); | 
|  | } | 
|  | } | 
|  |  | 
|  | // 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() { | 
|  | if (!isSimpleDigit(peekChar(0))) { | 
|  | return null; | 
|  | } | 
|  | int from = parseNumber(); | 
|  | skipWhitespace(); | 
|  | if (peekChar(0) != ':') { | 
|  | return cardinalRangeCache.get(from); | 
|  | } | 
|  | expect(':'); | 
|  | skipWhitespace(); | 
|  | int to = parseNumber(); | 
|  | if (from > to) { | 
|  | throw new ParseException("From is larger than to in range: " + from + ":" + to); | 
|  | } | 
|  | return nonCardinalRangeCache.get(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) { | 
|  | 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; | 
|  | } | 
|  | } | 
|  | } | 
|  | } |