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 
30 /**
31  * A representation of a proguard mapping for deobfuscating class names,
32  * field names, and stack frames.
33  */
34 public class ProguardMap {
35 
36   private static final String ARRAY_SYMBOL = "[]";
37 
38   private static class FrameData {
FrameData(String clearMethodName)39     public FrameData(String clearMethodName) {
40       this.clearMethodName = clearMethodName;
41     }
42 
43     private final String clearMethodName;
44     private final TreeMap<Integer, LineNumber> lineNumbers = new TreeMap<>();
45 
getClearLine(int obfuscatedLine)46     public int getClearLine(int obfuscatedLine) {
47       Map.Entry<Integer, LineNumber> lineNumberEntry = lineNumbers.floorEntry(obfuscatedLine);
48       LineNumber lineNumber = lineNumberEntry == null ? null : lineNumberEntry.getValue();
49       if (lineNumber != null
50           && obfuscatedLine >= lineNumber.obfuscatedLineStart
51           && obfuscatedLine <= lineNumber.obfuscatedLineEnd) {
52         return lineNumber.clearLineStart + obfuscatedLine - lineNumber.obfuscatedLineStart;
53       } else {
54         return obfuscatedLine;
55       }
56     }
57   }
58 
59   private static class LineNumber {
LineNumber(int obfuscatedLineStart, int obfuscatedLineEnd, int clearLineStart)60     public LineNumber(int obfuscatedLineStart, int obfuscatedLineEnd, int clearLineStart) {
61       this.obfuscatedLineStart = obfuscatedLineStart;
62       this.obfuscatedLineEnd = obfuscatedLineEnd;
63       this.clearLineStart = clearLineStart;
64     }
65 
66     private final int obfuscatedLineStart;
67     private final int obfuscatedLineEnd;
68     private final int clearLineStart;
69   }
70 
71   private static class ClassData {
72     private final String mClearName;
73 
74     // Mapping from obfuscated field name to clear field name.
75     private final Map<String, String> mFields = new HashMap<String, String>();
76 
77     // obfuscatedMethodName + clearSignature -> FrameData
78     private final Map<String, FrameData> mFrames = new HashMap<String, FrameData>();
79 
80     // Constructs a ClassData object for a class with the given clear name.
ClassData(String clearName)81     public ClassData(String clearName) {
82       mClearName = clearName;
83     }
84 
85     // Returns the clear name of the class.
getClearName()86     public String getClearName() {
87       return mClearName;
88     }
89 
addField(String obfuscatedName, String clearName)90     public void addField(String obfuscatedName, String clearName) {
91       mFields.put(obfuscatedName, clearName);
92     }
93 
94     // Get the clear name for the field in this class with the given
95     // obfuscated name. Returns the original obfuscated name if a clear
96     // name for the field could not be determined.
97     // TODO: Do we need to take into account the type of the field to
98     // propery determine the clear name?
getField(String obfuscatedName)99     public String getField(String obfuscatedName) {
100       String clearField = mFields.get(obfuscatedName);
101       return clearField == null ? obfuscatedName : clearField;
102     }
103 
addFrame(String obfuscatedMethodName, String clearMethodName, String clearSignature, int obfuscatedLine, int obfuscatedLineEnd, int clearLine)104     public void addFrame(String obfuscatedMethodName, String clearMethodName,
105             String clearSignature, int obfuscatedLine, int obfuscatedLineEnd, int clearLine) {
106         String key = obfuscatedMethodName + clearSignature;
107         FrameData data = mFrames.get(key);
108         if (data == null) {
109           data = new FrameData(clearMethodName);
110         }
111         data.lineNumbers.put(
112             obfuscatedLine, new LineNumber(obfuscatedLine, obfuscatedLineEnd, clearLine));
113         mFrames.put(key, data);
114     }
115 
getFrame(String clearClassName, String obfuscatedMethodName, String clearSignature, String obfuscatedFilename, int obfuscatedLine)116     public Frame getFrame(String clearClassName, String obfuscatedMethodName,
117         String clearSignature, String obfuscatedFilename, int obfuscatedLine) {
118       String key = obfuscatedMethodName + clearSignature;
119       FrameData frame = mFrames.get(key);
120       if (frame == null) {
121         frame = new FrameData(obfuscatedMethodName);
122       }
123       return new Frame(frame.clearMethodName, clearSignature,
124           getFileName(clearClassName), frame.getClearLine(obfuscatedLine));
125     }
126   }
127 
128   private Map<String, ClassData> mClassesFromClearName = new HashMap<String, ClassData>();
129   private Map<String, ClassData> mClassesFromObfuscatedName = new HashMap<String, ClassData>();
130 
131   /**
132    * Information associated with a stack frame that identifies a particular
133    * line of source code.
134    */
135   public static class Frame {
Frame(String method, String signature, String filename, int line)136     Frame(String method, String signature, String filename, int line) {
137       this.method = method;
138       this.signature = signature;
139       this.filename = filename;
140       this.line = line;
141     }
142 
143     /**
144      * The name of the method the stack frame belongs to.
145      * For example, "equals".
146      */
147     public final String method;
148 
149     /**
150      * The signature of the method the stack frame belongs to.
151      * For example, "(Ljava/lang/Object;)Z".
152      */
153     public final String signature;
154 
155     /**
156      * The name of the file with containing the line of source that the stack
157      * frame refers to.
158      */
159     public final String filename;
160 
161     /**
162      * The line number of the code in the source file that the stack frame
163      * refers to.
164      */
165     public final int line;
166   }
167 
parseException(String msg)168   private static void parseException(String msg) throws ParseException {
169     throw new ParseException(msg, 0);
170   }
171 
172   /**
173    * Creates a new empty proguard mapping.
174    * The {@link #readFromFile readFromFile} and
175    * {@link #readFromReader readFromReader} methods can be used to populate
176    * the proguard mapping with proguard mapping information.
177    */
ProguardMap()178   public ProguardMap() {
179   }
180 
181   /**
182    * Adds the proguard mapping information in <code>mapFile</code> to this
183    * proguard mapping.
184    * The <code>mapFile</code> should be a proguard mapping file generated with
185    * the <code>-printmapping</code> option when proguard was run.
186    *
187    * @param mapFile the name of a file with proguard mapping information
188    * @throws FileNotFoundException If the <code>mapFile</code> could not be
189    *                               found
190    * @throws IOException If an input exception occurred.
191    * @throws ParseException If the <code>mapFile</code> is not a properly
192    *                        formatted proguard mapping file.
193    */
readFromFile(File mapFile)194   public void readFromFile(File mapFile)
195     throws FileNotFoundException, IOException, ParseException {
196     readFromReader(new FileReader(mapFile));
197   }
198 
199   /**
200    * Adds the proguard mapping information read from <code>mapReader</code> to
201    * this proguard mapping.
202    * <code>mapReader</code> should be a Reader of a proguard mapping file
203    * generated with the <code>-printmapping</code> option when proguard was run.
204    *
205    * @param mapReader a Reader for reading the proguard mapping information
206    * @throws IOException If an input exception occurred.
207    * @throws ParseException If the <code>mapFile</code> is not a properly
208    *                        formatted proguard mapping file.
209    */
readFromReader(Reader mapReader)210   public void readFromReader(Reader mapReader) throws IOException, ParseException {
211     BufferedReader reader = new BufferedReader(mapReader);
212     String line = reader.readLine();
213     while (line != null) {
214       // Comment lines start with '#'. Skip over them.
215       if (line.startsWith("#")) {
216         line = reader.readLine();
217         continue;
218       }
219 
220       // Class lines are of the form:
221       //   'clear.class.name -> obfuscated_class_name:'
222       int sep = line.indexOf(" -> ");
223       if (sep == -1 || sep + 5 >= line.length()) {
224         parseException("Error parsing class line: '" + line + "'");
225       }
226       String clearClassName = line.substring(0, sep);
227       String obfuscatedClassName = line.substring(sep + 4, line.length() - 1);
228 
229       ClassData classData = new ClassData(clearClassName);
230       mClassesFromClearName.put(clearClassName, classData);
231       mClassesFromObfuscatedName.put(obfuscatedClassName, classData);
232 
233       // After the class line comes zero or more field/method lines of the form:
234       //   '    type clearName -> obfuscatedName'
235       line = reader.readLine();
236       while (line != null && line.startsWith("    ")) {
237         String trimmed = line.trim();
238         int ws = trimmed.indexOf(' ');
239         sep = trimmed.indexOf(" -> ");
240         if (ws == -1 || sep == -1) {
241           parseException("Error parse field/method line: '" + line + "'");
242         }
243 
244         String type = trimmed.substring(0, ws);
245         String clearName = trimmed.substring(ws + 1, sep);
246         String obfuscatedName = trimmed.substring(sep + 4, trimmed.length());
247 
248         // If the clearName contains '(', then this is for a method instead of a
249         // field.
250         if (clearName.indexOf('(') == -1) {
251           classData.addField(obfuscatedName, clearName);
252         } else {
253           // For methods, the type is of the form: [#:[#:]]<returnType>
254           int obfuscatedLine = 0;
255           // The end of the obfuscated line range.
256           // If line does not contain explicit end range, e.g #:, it is equivalent to #:#:
257           int obfuscatedLineEnd = 0;
258           int colon = type.indexOf(':');
259           if (colon != -1) {
260             obfuscatedLine = Integer.parseInt(type.substring(0, colon));
261             obfuscatedLineEnd = obfuscatedLine;
262             type = type.substring(colon + 1);
263           }
264           colon = type.indexOf(':');
265           if (colon != -1) {
266             obfuscatedLineEnd = Integer.parseInt(type.substring(0, colon));
267             type = type.substring(colon + 1);
268           }
269 
270           // For methods, the clearName is of the form: <clearName><sig>[:#[:#]]
271           int op = clearName.indexOf('(');
272           int cp = clearName.indexOf(')');
273           if (op == -1 || cp == -1) {
274             parseException("Error parse method line: '" + line + "'");
275           }
276 
277           String sig = clearName.substring(op, cp + 1);
278 
279           int clearLine = obfuscatedLine;
280           colon = clearName.lastIndexOf(':');
281           if (colon != -1) {
282             clearLine = Integer.parseInt(clearName.substring(colon + 1));
283             clearName = clearName.substring(0, colon);
284           }
285 
286           colon = clearName.lastIndexOf(':');
287           if (colon != -1) {
288             clearLine = Integer.parseInt(clearName.substring(colon + 1));
289             clearName = clearName.substring(0, colon);
290           }
291 
292           clearName = clearName.substring(0, op);
293 
294           String clearSig = fromProguardSignature(sig + type);
295           classData.addFrame(obfuscatedName, clearName, clearSig,
296                   obfuscatedLine, obfuscatedLineEnd, clearLine);
297         }
298 
299         line = reader.readLine();
300       }
301     }
302     reader.close();
303   }
304 
305   /**
306    * Returns the deobfuscated version of the given obfuscated class name.
307    * If this proguard mapping does not include information about how to
308    * deobfuscate the obfuscated class name, the obfuscated class name
309    * is returned.
310    *
311    * @param obfuscatedClassName the obfuscated class name to deobfuscate
312    * @return the deobfuscated class name.
313    */
getClassName(String obfuscatedClassName)314   public String getClassName(String obfuscatedClassName) {
315     // Class names for arrays may have trailing [] that need to be
316     // stripped before doing the lookup.
317     String baseName = obfuscatedClassName;
318     String arraySuffix = "";
319     while (baseName.endsWith(ARRAY_SYMBOL)) {
320       arraySuffix += ARRAY_SYMBOL;
321       baseName = baseName.substring(0, baseName.length() - ARRAY_SYMBOL.length());
322     }
323 
324     ClassData classData = mClassesFromObfuscatedName.get(baseName);
325     String clearBaseName = classData == null ? baseName : classData.getClearName();
326     return clearBaseName + arraySuffix;
327   }
328 
329   /**
330    * Returns the deobfuscated version of the obfuscated field name for the
331    * given deobfuscated class name.
332    * If this proguard mapping does not include information about how to
333    * deobfuscate the obfuscated field name, the obfuscated field name is
334    * returned.
335    *
336    * @param clearClass the deobfuscated name of the class the field belongs to
337    * @param obfuscatedField the obfuscated field name to deobfuscate
338    * @return the deobfuscated field name.
339    */
getFieldName(String clearClass, String obfuscatedField)340   public String getFieldName(String clearClass, String obfuscatedField) {
341     ClassData classData = mClassesFromClearName.get(clearClass);
342     if (classData == null) {
343       return obfuscatedField;
344     }
345     return classData.getField(obfuscatedField);
346   }
347 
348   /**
349    * Returns the deobfuscated version of the obfuscated stack frame
350    * information for the given deobfuscated class name.
351    * If this proguard mapping does not include information about how to
352    * deobfuscate the obfuscated stack frame information, the obfuscated stack
353    * frame information is returned.
354    *
355    * @param clearClassName the deobfuscated name of the class the stack frame's
356    * method belongs to
357    * @param obfuscatedMethodName the obfuscated method name to deobfuscate
358    * @param obfuscatedSignature the obfuscated method signature to deobfuscate
359    * @param obfuscatedFilename the obfuscated file name to deobfuscate.
360    * @param obfuscatedLine the obfuscated line number to deobfuscate.
361    * @return the deobfuscated stack frame information.
362    */
getFrame(String clearClassName, String obfuscatedMethodName, String obfuscatedSignature, String obfuscatedFilename, int obfuscatedLine)363   public Frame getFrame(String clearClassName, String obfuscatedMethodName,
364       String obfuscatedSignature, String obfuscatedFilename, int obfuscatedLine) {
365     String clearSignature = getSignature(obfuscatedSignature);
366     ClassData classData = mClassesFromClearName.get(clearClassName);
367     if (classData == null) {
368       return new Frame(obfuscatedMethodName, clearSignature,
369           obfuscatedFilename, obfuscatedLine);
370     }
371     return classData.getFrame(clearClassName, obfuscatedMethodName, clearSignature,
372         obfuscatedFilename, obfuscatedLine);
373   }
374 
375   // Converts a proguard-formatted method signature into a Java formatted
376   // method signature.
fromProguardSignature(String sig)377   private static String fromProguardSignature(String sig) throws ParseException {
378     if (sig.startsWith("(")) {
379       int end = sig.indexOf(')');
380       if (end == -1) {
381         parseException("Error parsing signature: " + sig);
382       }
383 
384       StringBuilder converted = new StringBuilder();
385       converted.append('(');
386       if (end > 1) {
387         for (String arg : sig.substring(1, end).split(",")) {
388           converted.append(fromProguardSignature(arg));
389         }
390       }
391       converted.append(')');
392       converted.append(fromProguardSignature(sig.substring(end + 1)));
393       return converted.toString();
394     } else if (sig.endsWith(ARRAY_SYMBOL)) {
395       return "[" + fromProguardSignature(sig.substring(0, sig.length() - 2));
396     } else if (sig.equals("boolean")) {
397       return "Z";
398     } else if (sig.equals("byte")) {
399       return "B";
400     } else if (sig.equals("char")) {
401       return "C";
402     } else if (sig.equals("short")) {
403       return "S";
404     } else if (sig.equals("int")) {
405       return "I";
406     } else if (sig.equals("long")) {
407       return "J";
408     } else if (sig.equals("float")) {
409       return "F";
410     } else if (sig.equals("double")) {
411       return "D";
412     } else if (sig.equals("void")) {
413       return "V";
414     } else {
415       return "L" + sig.replace('.', '/') + ";";
416     }
417   }
418 
419   // Return a clear signature for the given obfuscated signature.
getSignature(String obfuscatedSig)420   private String getSignature(String obfuscatedSig) {
421     StringBuilder builder = new StringBuilder();
422     for (int i = 0; i < obfuscatedSig.length(); i++) {
423       if (obfuscatedSig.charAt(i) == 'L') {
424         int e = obfuscatedSig.indexOf(';', i);
425         builder.append('L');
426         String cls = obfuscatedSig.substring(i + 1, e).replace('/', '.');
427         builder.append(getClassName(cls).replace('.', '/'));
428         builder.append(';');
429         i = e;
430       } else {
431         builder.append(obfuscatedSig.charAt(i));
432       }
433     }
434     return builder.toString();
435   }
436 
437   // Return a file name for the given clear class name.
getFileName(String clearClass)438   private static String getFileName(String clearClass) {
439     String filename = clearClass;
440     int dot = filename.lastIndexOf('.');
441     if (dot != -1) {
442       filename = filename.substring(dot + 1);
443     }
444 
445     int dollar = filename.indexOf('$');
446     if (dollar != -1) {
447       filename = filename.substring(0, dollar);
448     }
449     return filename + ".java";
450   }
451 }
452