• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2016 Google Inc.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.ahat.proguard;
18 
19 import java.io.BufferedReader;
20 import java.io.File;
21 import java.io.FileNotFoundException;
22 import java.io.FileReader;
23 import java.io.IOException;
24 import java.io.Reader;
25 import java.text.ParseException;
26 import java.util.HashMap;
27 import java.util.Map;
28 import java.util.TreeMap;
29 import java.util.regex.Matcher;
30 import java.util.regex.Pattern;
31 
32 /**
33  * A representation of a proguard mapping for deobfuscating class names,
34  * field names, and stack frames.
35  */
36 public class ProguardMap {
37 
38   private static final String ARRAY_SYMBOL = "[]";
39   private static final Version LINE_MAPPING_BEHAVIOR_CHANGE_VERSION = new Version(3, 1, 4);
40 
41   private static class FrameData {
FrameData(String clearMethodName)42     public FrameData(String clearMethodName) {
43       this.clearMethodName = clearMethodName;
44     }
45 
46     private final String clearMethodName;
47     private final TreeMap<Integer, LineNumberMapping> lineNumbers = new TreeMap<>();
48 
getClearLine(int obfuscatedLine)49     public int getClearLine(int obfuscatedLine) {
50       var lineNumberEntry = lineNumbers.floorEntry(obfuscatedLine);
51       LineNumberMapping mapping = lineNumberEntry == null ? null : lineNumberEntry.getValue();
52       if (mapping != null && mapping.hasObfuscatedLine(obfuscatedLine)) {
53         return mapping.mapObfuscatedLine(obfuscatedLine);
54       } else {
55         return obfuscatedLine;
56       }
57     }
58   }
59 
60   private static class LineRange {
LineRange(int start, int end)61     public LineRange(int start, int end) {
62       this.start = start;
63       this.end = end;
64     }
65 
hasLine(int lineNumber)66     public boolean hasLine(int lineNumber) {
67       return (lineNumber >= start && lineNumber <= end);
68     }
69 
70     public final int start;
71     public final int end;
72   }
73 
74   private static class LineNumberMapping {
LineNumberMapping(LineRange obfuscatedRange, LineRange clearRange)75     public LineNumberMapping(LineRange obfuscatedRange, LineRange clearRange) {
76       this.obfuscatedRange = obfuscatedRange;
77       this.clearRange = clearRange;
78     }
79 
hasObfuscatedLine(int lineNumber)80     public boolean hasObfuscatedLine(int lineNumber) {
81       return obfuscatedRange.hasLine(lineNumber);
82     }
83 
mapObfuscatedLine(int lineNumber)84     public int mapObfuscatedLine(int lineNumber) {
85       int mappedLine = clearRange.start + lineNumber - obfuscatedRange.start;
86       if (!clearRange.hasLine(mappedLine)) {
87         // If the mapped line ends out outside of range, it would be past the end, so just limit it
88         // to the end line
89         return clearRange.end;
90       }
91       return mappedLine;
92     }
93 
94     public final LineRange obfuscatedRange;
95     public final LineRange clearRange;
96   }
97 
98   private static class ClassData {
99     private final String mClearName;
100 
101     // Mapping from obfuscated field name to clear field name.
102     private final Map<String, String> mFields = new HashMap<String, String>();
103 
104     // obfuscatedMethodName + clearSignature -> FrameData
105     private final Map<String, FrameData> mFrames = new HashMap<String, FrameData>();
106 
107     // Constructs a ClassData object for a class with the given clear name.
ClassData(String clearName)108     public ClassData(String clearName) {
109       mClearName = clearName;
110     }
111 
112     // Returns the clear name of the class.
getClearName()113     public String getClearName() {
114       return mClearName;
115     }
116 
addField(String obfuscatedName, String clearName)117     public void addField(String obfuscatedName, String clearName) {
118       mFields.put(obfuscatedName, clearName);
119     }
120 
121     // Get the clear name for the field in this class with the given
122     // obfuscated name. Returns the original obfuscated name if a clear
123     // name for the field could not be determined.
124     // TODO: Do we need to take into account the type of the field to
125     // propery determine the clear name?
getField(String obfuscatedName)126     public String getField(String obfuscatedName) {
127       String clearField = mFields.get(obfuscatedName);
128       return clearField == null ? obfuscatedName : clearField;
129     }
130 
addFrame(String obfuscatedMethodName, String clearMethodName, String clearSignature, LineRange obfuscatedLine, LineRange clearRange)131     public void addFrame(String obfuscatedMethodName, String clearMethodName,
132             String clearSignature, LineRange obfuscatedLine, LineRange clearRange) {
133         String key = obfuscatedMethodName + clearSignature;
134         FrameData data = mFrames.get(key);
135         if (data == null) {
136           data = new FrameData(clearMethodName);
137         }
138         data.lineNumbers.put(
139             obfuscatedLine.start, new LineNumberMapping(obfuscatedLine, clearRange));
140         mFrames.put(key, data);
141     }
142 
getFrame(String clearClassName, String obfuscatedMethodName, String clearSignature, String obfuscatedFilename, int obfuscatedLine)143     public Frame getFrame(String clearClassName, String obfuscatedMethodName,
144         String clearSignature, String obfuscatedFilename, int obfuscatedLine) {
145       String key = obfuscatedMethodName + clearSignature;
146       FrameData frame = mFrames.get(key);
147       if (frame == null) {
148         frame = new FrameData(obfuscatedMethodName);
149       }
150       return new Frame(frame.clearMethodName, clearSignature,
151           getFileName(clearClassName), frame.getClearLine(obfuscatedLine));
152     }
153   }
154 
155   private Map<String, ClassData> mClassesFromClearName = new HashMap<String, ClassData>();
156   private Map<String, ClassData> mClassesFromObfuscatedName = new HashMap<String, ClassData>();
157 
158   /**
159    * Information associated with a stack frame that identifies a particular
160    * line of source code.
161    */
162   public static class Frame {
Frame(String method, String signature, String filename, int line)163     Frame(String method, String signature, String filename, int line) {
164       this.method = method;
165       this.signature = signature;
166       this.filename = filename;
167       this.line = line;
168     }
169 
170     /**
171      * The name of the method the stack frame belongs to.
172      * For example, "equals".
173      */
174     public final String method;
175 
176     /**
177      * The signature of the method the stack frame belongs to.
178      * For example, "(Ljava/lang/Object;)Z".
179      */
180     public final String signature;
181 
182     /**
183      * The name of the file with containing the line of source that the stack
184      * frame refers to.
185      */
186     public final String filename;
187 
188     /**
189      * The line number of the code in the source file that the stack frame
190      * refers to.
191      */
192     public final int line;
193   }
194 
parseException(String msg)195   private static void parseException(String msg) throws ParseException {
196     throw new ParseException(msg, 0);
197   }
198 
199   /**
200    * Creates a new empty proguard mapping.
201    * The {@link #readFromFile readFromFile} and
202    * {@link #readFromReader readFromReader} methods can be used to populate
203    * the proguard mapping with proguard mapping information.
204    */
ProguardMap()205   public ProguardMap() {
206   }
207 
208   /**
209    * Adds the proguard mapping information in <code>mapFile</code> to this
210    * proguard mapping.
211    * The <code>mapFile</code> should be a proguard mapping file generated with
212    * the <code>-printmapping</code> option when proguard was run.
213    *
214    * @param mapFile the name of a file with proguard mapping information
215    * @throws FileNotFoundException If the <code>mapFile</code> could not be
216    *                               found
217    * @throws IOException If an input exception occurred.
218    * @throws ParseException If the <code>mapFile</code> is not a properly
219    *                        formatted proguard mapping file.
220    */
readFromFile(File mapFile)221   public void readFromFile(File mapFile)
222     throws FileNotFoundException, IOException, ParseException {
223     readFromReader(new FileReader(mapFile));
224   }
225 
226   /**
227    * Adds the proguard mapping information read from <code>mapReader</code> to
228    * this proguard mapping.
229    * <code>mapReader</code> should be a Reader of a proguard mapping file
230    * generated with the <code>-printmapping</code> option when proguard was run.
231    *
232    * @param mapReader a Reader for reading the proguard mapping information
233    * @throws IOException If an input exception occurred.
234    * @throws ParseException If the <code>mapFile</code> is not a properly
235    *                        formatted proguard mapping file.
236    */
readFromReader(Reader mapReader)237   public void readFromReader(Reader mapReader) throws IOException, ParseException {
238     Version compilerVersion = new Version(0, 0, 0);
239     BufferedReader reader = new BufferedReader(mapReader);
240     String line = reader.readLine();
241     while (line != null) {
242       // Skip comment lines.
243       if (isCommentLine(line)) {
244         compilerVersion = tryParseVersion(line, compilerVersion);
245         line = reader.readLine();
246         continue;
247       }
248 
249       // Class lines are of the form:
250       //   'clear.class.name -> obfuscated_class_name:'
251       int sep = line.indexOf(" -> ");
252       if (sep == -1 || sep + 5 >= line.length()) {
253         parseException("Error parsing class line: '" + line + "'");
254       }
255       String clearClassName = line.substring(0, sep);
256       String obfuscatedClassName = line.substring(sep + 4, line.length() - 1);
257 
258       ClassData classData = new ClassData(clearClassName);
259       mClassesFromClearName.put(clearClassName, classData);
260       mClassesFromObfuscatedName.put(obfuscatedClassName, classData);
261 
262       // After the class line comes zero or more field/method lines of the form:
263       //   '    type clearName -> obfuscatedName'
264       //   '# comment line'
265       line = reader.readLine();
266       while (line != null && (line.startsWith("    ") || isCommentLine(line))) {
267         String trimmed = line.trim();
268         // Comment lines may occur anywhere in the file.
269         // Skip over them.
270         if (isCommentLine(trimmed)) {
271           line = reader.readLine();
272           continue;
273         }
274         int ws = trimmed.indexOf(' ');
275         sep = trimmed.indexOf(" -> ");
276         if (ws == -1 || sep == -1) {
277           parseException("Error parse field/method line: '" + line + "'");
278         }
279 
280         String type = trimmed.substring(0, ws);
281         String clearName = trimmed.substring(ws + 1, sep);
282         String obfuscatedName = trimmed.substring(sep + 4, trimmed.length());
283 
284         // If the clearName contains '(', then this is for a method instead of a
285         // field.
286         if (clearName.indexOf('(') == -1) {
287           classData.addField(obfuscatedName, clearName);
288         } else {
289           // For methods, the type is of the form: [#:[#:]]<returnType>
290           int obfuscatedLineStart = 0;
291           // The end of the obfuscated line range.
292           // If line does not contain explicit end range, e.g #:, it is equivalent to #:#:
293           int obfuscatedLineEnd = 0;
294           int colon = type.indexOf(':');
295           if (colon != -1) {
296             obfuscatedLineStart = Integer.parseInt(type.substring(0, colon));
297             obfuscatedLineEnd = obfuscatedLineStart;
298             type = type.substring(colon + 1);
299           }
300           colon = type.indexOf(':');
301           if (colon != -1) {
302             obfuscatedLineEnd = Integer.parseInt(type.substring(0, colon));
303             type = type.substring(colon + 1);
304           }
305           LineRange obfuscatedRange = new LineRange(obfuscatedLineStart, obfuscatedLineEnd);
306 
307           // For methods, the clearName is of the form: <clearName><sig>[:#[:#]]
308           int op = clearName.indexOf('(');
309           int cp = clearName.indexOf(')');
310           if (op == -1 || cp == -1) {
311             parseException("Error parse method line: '" + line + "'");
312           }
313 
314           String sig = clearName.substring(op, cp + 1);
315 
316           int clearLineStart = obfuscatedRange.start;
317           int clearLineEnd = obfuscatedRange.end;
318           colon = clearName.lastIndexOf(':');
319           if (colon != -1) {
320             if (compilerVersion.compareTo(LINE_MAPPING_BEHAVIOR_CHANGE_VERSION) < 0) {
321               // Before v3.1.4 if only one clear line was present, that implied a range equal to the
322               // obfuscated line range
323               clearLineStart = Integer.parseInt(clearName.substring(colon + 1));
324               clearLineEnd = clearLineStart + obfuscatedRange.end - obfuscatedRange.start;
325             } else {
326               // From v3.1.4 if only one clear line was present, that implies that all lines map to
327               // a single clear line
328               clearLineEnd = Integer.parseInt(clearName.substring(colon + 1));
329               clearLineStart = clearLineEnd;
330             }
331             clearName = clearName.substring(0, colon);
332           }
333 
334           colon = clearName.lastIndexOf(':');
335           if (colon != -1) {
336             clearLineStart = Integer.parseInt(clearName.substring(colon + 1));
337             clearName = clearName.substring(0, colon);
338           }
339           LineRange clearRange = new LineRange(clearLineStart, clearLineEnd);
340 
341           clearName = clearName.substring(0, op);
342 
343           String clearSig = fromProguardSignature(sig + type);
344           classData.addFrame(obfuscatedName, clearName, clearSig, obfuscatedRange, clearRange);
345         }
346 
347         line = reader.readLine();
348       }
349     }
350     reader.close();
351   }
352 
353   private static class Version implements Comparable<Version> {
354     final int major;
355     final int minor;
356     final int build;
357 
Version(int major, int minor, int build)358     public Version(int major, int minor, int build) {
359       this.major = major;
360       this.minor = minor;
361       this.build = build;
362     }
363 
364     @Override
compareTo(Version other)365     public int compareTo(Version other) {
366       int compare = Integer.compare(this.major, other.major);
367       if (compare == 0) {
368         compare = Integer.compare(this.minor, other.minor);
369       }
370       if (compare == 0) {
371         compare = Integer.compare(this.build, other.build);
372       }
373       return compare;
374     }
375   }
376 
isCommentLine(String line)377   private boolean isCommentLine(String line) {
378     // Comment lines start with '#' and my have leading whitespaces.
379     return line.trim().startsWith("#");
380   }
381 
tryParseVersion(String line, Version old)382   private Version tryParseVersion(String line, Version old) {
383     Pattern pattern = Pattern.compile("#\\s*compiler_version:\\s*(\\d+).(\\d+).(?:(\\d+))?");
384     Matcher matcher = pattern.matcher(line);
385     if (matcher.find()) {
386       String buildStr = matcher.group(3);
387       if (buildStr == null) {
388         buildStr = Integer.toString(0);
389       }
390       return new Version(
391           Integer.parseInt(matcher.group(1)),
392           Integer.parseInt(matcher.group(2)),
393           Integer.parseInt(buildStr));
394     }
395     return old;
396   }
397 
398   /**
399    * Returns the deobfuscated version of the given obfuscated class name.
400    * If this proguard mapping does not include information about how to
401    * deobfuscate the obfuscated class name, the obfuscated class name
402    * is returned.
403    *
404    * @param obfuscatedClassName the obfuscated class name to deobfuscate
405    * @return the deobfuscated class name.
406    */
getClassName(String obfuscatedClassName)407   public String getClassName(String obfuscatedClassName) {
408     // Class names for arrays may have trailing [] that need to be
409     // stripped before doing the lookup.
410     String baseName = obfuscatedClassName;
411     String arraySuffix = "";
412     while (baseName.endsWith(ARRAY_SYMBOL)) {
413       arraySuffix += ARRAY_SYMBOL;
414       baseName = baseName.substring(0, baseName.length() - ARRAY_SYMBOL.length());
415     }
416 
417     ClassData classData = mClassesFromObfuscatedName.get(baseName);
418     String clearBaseName = classData == null ? baseName : classData.getClearName();
419     return clearBaseName + arraySuffix;
420   }
421 
422   /**
423    * Returns the deobfuscated version of the obfuscated field name for the
424    * given deobfuscated class name.
425    * If this proguard mapping does not include information about how to
426    * deobfuscate the obfuscated field name, the obfuscated field name is
427    * returned.
428    *
429    * @param clearClass the deobfuscated name of the class the field belongs to
430    * @param obfuscatedField the obfuscated field name to deobfuscate
431    * @return the deobfuscated field name.
432    */
getFieldName(String clearClass, String obfuscatedField)433   public String getFieldName(String clearClass, String obfuscatedField) {
434     ClassData classData = mClassesFromClearName.get(clearClass);
435     if (classData == null) {
436       return obfuscatedField;
437     }
438     return classData.getField(obfuscatedField);
439   }
440 
441   /**
442    * Returns the deobfuscated version of the obfuscated stack frame
443    * information for the given deobfuscated class name.
444    * If this proguard mapping does not include information about how to
445    * deobfuscate the obfuscated stack frame information, the obfuscated stack
446    * frame information is returned.
447    *
448    * @param clearClassName the deobfuscated name of the class the stack frame's
449    * method belongs to
450    * @param obfuscatedMethodName the obfuscated method name to deobfuscate
451    * @param obfuscatedSignature the obfuscated method signature to deobfuscate
452    * @param obfuscatedFilename the obfuscated file name to deobfuscate.
453    * @param obfuscatedLine the obfuscated line number to deobfuscate.
454    * @return the deobfuscated stack frame information.
455    */
getFrame(String clearClassName, String obfuscatedMethodName, String obfuscatedSignature, String obfuscatedFilename, int obfuscatedLine)456   public Frame getFrame(String clearClassName, String obfuscatedMethodName,
457       String obfuscatedSignature, String obfuscatedFilename, int obfuscatedLine) {
458     String clearSignature = getSignature(obfuscatedSignature);
459     ClassData classData = mClassesFromClearName.get(clearClassName);
460     if (classData == null) {
461       return new Frame(obfuscatedMethodName, clearSignature,
462           obfuscatedFilename, obfuscatedLine);
463     }
464     return classData.getFrame(clearClassName, obfuscatedMethodName, clearSignature,
465         obfuscatedFilename, obfuscatedLine);
466   }
467 
468   // Converts a proguard-formatted method signature into a Java formatted
469   // method signature.
fromProguardSignature(String sig)470   private static String fromProguardSignature(String sig) throws ParseException {
471     if (sig.startsWith("(")) {
472       int end = sig.indexOf(')');
473       if (end == -1) {
474         parseException("Error parsing signature: " + sig);
475       }
476 
477       StringBuilder converted = new StringBuilder();
478       converted.append('(');
479       if (end > 1) {
480         for (String arg : sig.substring(1, end).split(",")) {
481           converted.append(fromProguardSignature(arg));
482         }
483       }
484       converted.append(')');
485       converted.append(fromProguardSignature(sig.substring(end + 1)));
486       return converted.toString();
487     } else if (sig.endsWith(ARRAY_SYMBOL)) {
488       return "[" + fromProguardSignature(sig.substring(0, sig.length() - 2));
489     } else if (sig.equals("boolean")) {
490       return "Z";
491     } else if (sig.equals("byte")) {
492       return "B";
493     } else if (sig.equals("char")) {
494       return "C";
495     } else if (sig.equals("short")) {
496       return "S";
497     } else if (sig.equals("int")) {
498       return "I";
499     } else if (sig.equals("long")) {
500       return "J";
501     } else if (sig.equals("float")) {
502       return "F";
503     } else if (sig.equals("double")) {
504       return "D";
505     } else if (sig.equals("void")) {
506       return "V";
507     } else {
508       return "L" + sig.replace('.', '/') + ";";
509     }
510   }
511 
512   // Return a clear signature for the given obfuscated signature.
getSignature(String obfuscatedSig)513   private String getSignature(String obfuscatedSig) {
514     StringBuilder builder = new StringBuilder();
515     for (int i = 0; i < obfuscatedSig.length(); i++) {
516       if (obfuscatedSig.charAt(i) == 'L') {
517         int e = obfuscatedSig.indexOf(';', i);
518         builder.append('L');
519         String cls = obfuscatedSig.substring(i + 1, e).replace('/', '.');
520         builder.append(getClassName(cls).replace('.', '/'));
521         builder.append(';');
522         i = e;
523       } else {
524         builder.append(obfuscatedSig.charAt(i));
525       }
526     }
527     return builder.toString();
528   }
529 
530   // Return a file name for the given clear class name.
getFileName(String clearClass)531   private static String getFileName(String clearClass) {
532     String filename = clearClass;
533     int dot = filename.lastIndexOf('.');
534     if (dot != -1) {
535       filename = filename.substring(dot + 1);
536     }
537 
538     int dollar = filename.indexOf('$');
539     if (dollar != -1) {
540       filename = filename.substring(0, dollar);
541     }
542     return filename + ".java";
543   }
544 }
545