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