• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 // Copyright (c) 2016, the R8 project authors. Please see the AUTHORS file
2 // for details. All rights reserved. Use of this source code is governed by a
3 // BSD-style license that can be found in the LICENSE file.
4 package com.android.tools.r8.naming;
5 
6 import com.android.tools.r8.logging.Log;
7 import com.android.tools.r8.naming.MemberNaming.FieldSignature;
8 import com.android.tools.r8.naming.MemberNaming.MethodSignature;
9 import com.android.tools.r8.naming.MemberNaming.Range;
10 import com.android.tools.r8.naming.MemberNaming.Signature;
11 import com.android.tools.r8.naming.MemberNaming.SingleLineRange;
12 import com.google.common.collect.ImmutableMap;
13 import java.io.BufferedReader;
14 import java.io.ByteArrayInputStream;
15 import java.io.IOException;
16 import java.io.InputStream;
17 import java.io.InputStreamReader;
18 import java.nio.charset.StandardCharsets;
19 import java.nio.file.Files;
20 import java.nio.file.Path;
21 import java.util.ArrayList;
22 import java.util.HashMap;
23 import java.util.LinkedList;
24 import java.util.List;
25 import java.util.Map;
26 import java.util.function.Consumer;
27 
28 /**
29  * Parses a Proguard mapping file and produces mappings from obfuscated class names to the original
30  * name and from obfuscated member signatures to the original members the obfuscated member
31  * was formed of.
32  * <p>
33  * The expected format is as follows
34  * <p>
35  * original-type-name ARROW obfuscated-type-name COLON starts a class mapping
36  * description and maps original to obfuscated.
37  * <p>
38  * followed by one or more of
39  * <p>
40  * signature ARROW name
41  * <p>
42  * which maps the member with the given signature to the new name. This mapping is not
43  * bidirectional as member names are overloaded by signature. To make it bidirectional, we extend
44  * the name with the signature of the original member.
45  * <p>
46  * Due to inlining, we might have the above prefixed with a range (two numbers separated by :).
47  * <p>
48  * range COLON signature ARROW name
49  * <p>
50  * This has the same meaning as the above but also encodes the line number range of the member. This
51  * may be followed by multiple inline mappings of the form
52  * <p>
53  * range COLON signature COLON range ARROW name
54  * <p>
55  * to identify that signature was inlined from the second range to the new line numbers in the first
56  * range. This is then followed by information on the call trace to where the member was inlined.
57  * These entries have the form
58  * <p>
59  * range COLON signature COLON number ARROW name
60  * <p>
61  * and are currently only stored to be able to reproduce them later.
62  */
63 public class ProguardMapReader implements AutoCloseable {
64 
65   private final BufferedReader reader;
66 
close()67   public void close() throws IOException {
68     if (reader != null) {
69       reader.close();
70     }
71   }
72 
ProguardMapReader(BufferedReader reader)73   private ProguardMapReader(BufferedReader reader) throws IOException {
74     this.reader = reader;
75   }
76 
mapperFromInputStream(InputStream in)77   public static ClassNameMapper mapperFromInputStream(InputStream in) throws IOException {
78     BufferedReader reader = new BufferedReader(new InputStreamReader(in, "UTF8"));
79     try (ProguardMapReader proguardReader = new ProguardMapReader(reader)) {
80       return proguardReader.parse();
81     }
82   }
83 
mapperFromFile(Path path)84   public static ClassNameMapper mapperFromFile(Path path) throws IOException {
85     return mapperFromInputStream(Files.newInputStream(path));
86   }
87 
mapperFromString(String contents)88   public static ClassNameMapper mapperFromString(String contents) throws IOException {
89     return mapperFromInputStream(
90         new ByteArrayInputStream(contents.getBytes(StandardCharsets.UTF_8)));
91   }
92 
93   // Internal parser state
94   private int lineNo = 0;
95   private int lineOffset = 0;
96   private String line;
97 
peek()98   private char peek() {
99     return peek(0);
100   }
101 
peek(int distance)102   private char peek(int distance) {
103     return lineOffset + distance < line.length()
104         ? line.charAt(lineOffset + distance)
105         : '\n';
106   }
107 
next()108   private char next() {
109     try {
110       return line.charAt(lineOffset++);
111     } catch (ArrayIndexOutOfBoundsException e) {
112       throw new ParseException("Unexpected end of line");
113     }
114   }
115 
nextLine()116   private boolean nextLine() throws IOException {
117     if (line.length() != lineOffset) {
118       throw new ParseException("Expected end of line");
119     }
120     return skipLine();
121   }
122 
skipLine()123   private boolean skipLine() throws IOException {
124     lineNo++;
125     lineOffset = 0;
126     line = reader.readLine();
127     return hasLine();
128   }
129 
hasLine()130   private boolean hasLine() {
131     return line != null;
132   }
133 
134   // Helpers for common pattern
skipWhitespace()135   private void skipWhitespace() {
136     while (Character.isWhitespace(peek())) {
137       next();
138     }
139   }
140 
expect(char c)141   private char expect(char c) {
142     if (next() != c) {
143       throw new ParseException("Expected '" + c + "'");
144     }
145     return c;
146   }
147 
parse()148   public ClassNameMapper parse() throws IOException {
149     // Read the first line.
150     line = reader.readLine();
151     Map<String, ClassNaming> classNames = parseClassMappings();
152     return new ClassNameMapper(classNames);
153   }
154 
155   // Parsing of entries
156 
parseClassMappings()157   private Map<String, ClassNaming> parseClassMappings() throws IOException {
158     ImmutableMap.Builder<String, ClassNaming> builder = ImmutableMap.builder();
159     while (hasLine()) {
160       String before = parseType(false);
161       skipWhitespace();
162       // Workaround for proguard map files that contain entries for package-info.java files.
163       if (!acceptArrow()) {
164         // If this was a package-info line, we parsed the "package" string.
165         if (!before.endsWith("package") || !acceptString("-info")) {
166           throw new ParseException("Expected arrow after class name " + before);
167         }
168         skipLine();
169         continue;
170       }
171       skipWhitespace();
172       String after = parseType(false);
173       expect(':');
174       ClassNaming currentClass = new ClassNaming(after, before);
175       builder.put(after, currentClass);
176       if (nextLine()) {
177         parseMemberMappings(currentClass);
178       }
179     }
180     return builder.build();
181   }
182 
parseMemberMappings(ClassNaming currentClass)183   private void parseMemberMappings(ClassNaming currentClass) throws IOException {
184     MemberNaming current = null;
185     Range previousInlineRange = null;
186     Signature previousSignature = null;
187     String previousRenamedName = null;
188     List<Consumer<MemberNaming>> collectedInfos = new ArrayList<>(10);
189 
190     while (Character.isWhitespace(peek())) {
191       skipWhitespace();
192       Range inlinedLineRange = maybeParseRange();
193       if (inlinedLineRange != null) {
194         expect(':');
195       }
196       Signature signature = parseSignature();
197       Range originalLineRange;
198       if (peek() == ':') {
199         // This is an inlining definition
200         next();
201         originalLineRange = maybeParseRange();
202         if (originalLineRange == null) {
203           if (!skipLine()) {
204             break;
205           }
206           continue;
207         }
208       } else {
209         originalLineRange = null;
210       }
211       skipWhitespace();
212       skipArrow();
213       skipWhitespace();
214       String renamedName = parseMethodName();
215       // If there is no line number information at the front or if it changes, we have a new
216       // segment. Likewise, if the range information on the right hand side has two values, we have
217       // a new segment.
218       if (inlinedLineRange == null
219           || previousInlineRange == null
220           || originalLineRange == null
221           || !previousInlineRange.equals(inlinedLineRange)
222           || !originalLineRange.isSingle()) {
223         // We are at a range boundary. Either we parsed something new, or an inline frame is over.
224         // We detect this by checking whether the previous signature matches the one of current.
225         if (current == null || !previousSignature.equals(current.signature)) {
226           if (collectedInfos.size() == 1) {
227             current = new MemberNaming(previousSignature, previousRenamedName, previousInlineRange);
228             currentClass.addMemberEntry(current);
229           } else {
230             if (Log.ENABLED && !collectedInfos.isEmpty()) {
231               Log.warn(getClass(),
232                   "More than one member entry that forms a new group at %s %s -> %s",
233                   previousInlineRange, previousSignature, previousRenamedName);
234             }
235           }
236         } else {
237           MemberNaming finalCurrent = current;
238           collectedInfos.forEach(info -> info.accept(finalCurrent));
239         }
240         collectedInfos.clear();
241       }
242       // Defer the creation of the info until we have the correct member.
243       collectedInfos.add((m) -> m.addInliningRange(inlinedLineRange, signature, originalLineRange));
244       // We have parsed the whole line, move on.
245       previousInlineRange = inlinedLineRange;
246       previousSignature = signature;
247       previousRenamedName = renamedName;
248       if (!nextLine()) {
249         break;
250       }
251     }
252     // Process the last round if lines have been read.
253     if (current == null || !previousSignature.equals(current.signature)) {
254       if (collectedInfos.size() == 1) {
255         current = new MemberNaming(previousSignature, previousRenamedName, previousInlineRange);
256         currentClass.addMemberEntry(current);
257       }
258     } else {
259       MemberNaming finalCurrent = current;
260       collectedInfos.forEach(info -> info.accept(finalCurrent));
261     }
262     collectedInfos.clear();
263   }
264 
265   // Parsing of components
266 
skipIdentifier(boolean allowInit)267   private void skipIdentifier(boolean allowInit) {
268     boolean isInit = false;
269     if (allowInit && peek() == '<') {
270       // swallow the leading < character
271       next();
272       isInit = true;
273     }
274     if (!Character.isJavaIdentifierStart(peek())) {
275       throw new ParseException("Identifier expected");
276     }
277     next();
278     while (Character.isJavaIdentifierPart(peek())) {
279       next();
280     }
281     if (isInit) {
282       expect('>');
283     }
284     if (Character.isJavaIdentifierPart(peek())) {
285       throw new ParseException("End of identifier expected");
286     }
287   }
288 
289   // Cache for canonicalizing strings.
290   // This saves 10% of heap space for large programs.
291   final HashMap<String, String> cache = new HashMap<>();
292 
substring(int start)293   private String substring(int start) {
294     String result = line.substring(start, lineOffset);
295     if (cache.containsKey(result)) {
296       return cache.get(result);
297     }
298     cache.put(result, result);
299     return result;
300   }
301 
parseMethodName()302   private String parseMethodName() {
303     int startPosition = lineOffset;
304     skipIdentifier(true);
305     while (peek() == '.') {
306       next();
307       skipIdentifier(true);
308     }
309     return substring(startPosition);
310   }
311 
parseType(boolean allowArray)312   private String parseType(boolean allowArray) {
313     int startPosition = lineOffset;
314     skipIdentifier(false);
315     while (peek() == '.') {
316       next();
317       skipIdentifier(false);
318     }
319     if (allowArray) {
320       while (peek() == '[') {
321         next();
322         expect(']');
323       }
324     }
325     return substring(startPosition);
326   }
327 
parseSignature()328   private Signature parseSignature() {
329     String type = parseType(true);
330     expect(' ');
331     String name = parseMethodName();
332     Signature signature;
333     if (peek() == '(') {
334       next();
335       String[] arguments;
336       if (peek() == ')') {
337         arguments = new String[0];
338       } else {
339         List<String> items = new LinkedList<>();
340         items.add(parseType(true));
341         while (peek() != ')') {
342           expect(',');
343           items.add(parseType(true));
344         }
345         arguments = items.toArray(new String[items.size()]);
346       }
347       expect(')');
348       signature = new MethodSignature(name, type, arguments);
349     } else {
350       signature = new FieldSignature(name, type);
351     }
352     return signature;
353   }
354 
skipArrow()355   private void skipArrow() {
356     expect('-');
357     expect('>');
358   }
359 
acceptArrow()360   private boolean acceptArrow() {
361     if (peek() == '-' && peek(1) == '>') {
362       next();
363       next();
364       return true;
365     }
366     return false;
367   }
368 
acceptString(String s)369   private boolean acceptString(String s) {
370     for (int i = 0; i < s.length(); i++) {
371       if (peek(i) != s.charAt(i)) {
372         return false;
373       }
374     }
375     for (int i = 0; i < s.length(); i++) {
376       next();
377     }
378     return true;
379   }
380 
maybeParseRange()381   private Range maybeParseRange() {
382     if (!Character.isDigit(peek())) {
383       return null;
384     }
385     int from = parseNumber();
386     if (peek() != ':') {
387       return new SingleLineRange(from);
388     }
389     expect(':');
390     int to = parseNumber();
391     return new Range(from, to);
392   }
393 
parseNumber()394   private int parseNumber() {
395     int result = 0;
396     if (!Character.isDigit(peek())) {
397       throw new ParseException("Number expected");
398     }
399     do {
400       result *= 10;
401       result += Character.getNumericValue(next());
402     } while (Character.isDigit(peek()));
403     return result;
404   }
405 
406   private class ParseException extends RuntimeException {
407 
408     private final int lineNo;
409     private final int lineOffset;
410     private final String msg;
411 
ParseException(String msg)412     ParseException(String msg) {
413       lineNo = ProguardMapReader.this.lineNo;
414       lineOffset = ProguardMapReader.this.lineOffset;
415       this.msg = msg;
416     }
417 
toString()418     public String toString() {
419       return "Parse error [" + lineNo + ":" + lineOffset + "] " + msg;
420     }
421   }
422 }
423