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       //   '# comment line'
236       line = reader.readLine();
237       while (line != null && (line.startsWith("    ") || line.startsWith("#"))) {
238         // Comment lines start with '#' and may occur anywhere in the file.
239         // Skip over them.
240         if (line.startsWith("#")) {
241           line = reader.readLine();
242           continue;
243         }
244         String trimmed = line.trim();
245         int ws = trimmed.indexOf(' ');
246         sep = trimmed.indexOf(" -> ");
247         if (ws == -1 || sep == -1) {
248           parseException("Error parse field/method line: '" + line + "'");
249         }
250 
251         String type = trimmed.substring(0, ws);
252         String clearName = trimmed.substring(ws + 1, sep);
253         String obfuscatedName = trimmed.substring(sep + 4, trimmed.length());
254 
255         // If the clearName contains '(', then this is for a method instead of a
256         // field.
257         if (clearName.indexOf('(') == -1) {
258           classData.addField(obfuscatedName, clearName);
259         } else {
260           // For methods, the type is of the form: [#:[#:]]<returnType>
261           int obfuscatedLine = 0;
262           // The end of the obfuscated line range.
263           // If line does not contain explicit end range, e.g #:, it is equivalent to #:#:
264           int obfuscatedLineEnd = 0;
265           int colon = type.indexOf(':');
266           if (colon != -1) {
267             obfuscatedLine = Integer.parseInt(type.substring(0, colon));
268             obfuscatedLineEnd = obfuscatedLine;
269             type = type.substring(colon + 1);
270           }
271           colon = type.indexOf(':');
272           if (colon != -1) {
273             obfuscatedLineEnd = Integer.parseInt(type.substring(0, colon));
274             type = type.substring(colon + 1);
275           }
276 
277           // For methods, the clearName is of the form: <clearName><sig>[:#[:#]]
278           int op = clearName.indexOf('(');
279           int cp = clearName.indexOf(')');
280           if (op == -1 || cp == -1) {
281             parseException("Error parse method line: '" + line + "'");
282           }
283 
284           String sig = clearName.substring(op, cp + 1);
285 
286           int clearLine = obfuscatedLine;
287           colon = clearName.lastIndexOf(':');
288           if (colon != -1) {
289             clearLine = Integer.parseInt(clearName.substring(colon + 1));
290             clearName = clearName.substring(0, colon);
291           }
292 
293           colon = clearName.lastIndexOf(':');
294           if (colon != -1) {
295             clearLine = Integer.parseInt(clearName.substring(colon + 1));
296             clearName = clearName.substring(0, colon);
297           }
298 
299           clearName = clearName.substring(0, op);
300 
301           String clearSig = fromProguardSignature(sig + type);
302           classData.addFrame(obfuscatedName, clearName, clearSig,
303                   obfuscatedLine, obfuscatedLineEnd, clearLine);
304         }
305 
306         line = reader.readLine();
307       }
308     }
309     reader.close();
310   }
311 
312   /**
313    * Returns the deobfuscated version of the given obfuscated class name.
314    * If this proguard mapping does not include information about how to
315    * deobfuscate the obfuscated class name, the obfuscated class name
316    * is returned.
317    *
318    * @param obfuscatedClassName the obfuscated class name to deobfuscate
319    * @return the deobfuscated class name.
320    */
getClassName(String obfuscatedClassName)321   public String getClassName(String obfuscatedClassName) {
322     // Class names for arrays may have trailing [] that need to be
323     // stripped before doing the lookup.
324     String baseName = obfuscatedClassName;
325     String arraySuffix = "";
326     while (baseName.endsWith(ARRAY_SYMBOL)) {
327       arraySuffix += ARRAY_SYMBOL;
328       baseName = baseName.substring(0, baseName.length() - ARRAY_SYMBOL.length());
329     }
330 
331     ClassData classData = mClassesFromObfuscatedName.get(baseName);
332     String clearBaseName = classData == null ? baseName : classData.getClearName();
333     return clearBaseName + arraySuffix;
334   }
335 
336   /**
337    * Returns the deobfuscated version of the obfuscated field name for the
338    * given deobfuscated class name.
339    * If this proguard mapping does not include information about how to
340    * deobfuscate the obfuscated field name, the obfuscated field name is
341    * returned.
342    *
343    * @param clearClass the deobfuscated name of the class the field belongs to
344    * @param obfuscatedField the obfuscated field name to deobfuscate
345    * @return the deobfuscated field name.
346    */
getFieldName(String clearClass, String obfuscatedField)347   public String getFieldName(String clearClass, String obfuscatedField) {
348     ClassData classData = mClassesFromClearName.get(clearClass);
349     if (classData == null) {
350       return obfuscatedField;
351     }
352     return classData.getField(obfuscatedField);
353   }
354 
355   /**
356    * Returns the deobfuscated version of the obfuscated stack frame
357    * information for the given deobfuscated class name.
358    * If this proguard mapping does not include information about how to
359    * deobfuscate the obfuscated stack frame information, the obfuscated stack
360    * frame information is returned.
361    *
362    * @param clearClassName the deobfuscated name of the class the stack frame's
363    * method belongs to
364    * @param obfuscatedMethodName the obfuscated method name to deobfuscate
365    * @param obfuscatedSignature the obfuscated method signature to deobfuscate
366    * @param obfuscatedFilename the obfuscated file name to deobfuscate.
367    * @param obfuscatedLine the obfuscated line number to deobfuscate.
368    * @return the deobfuscated stack frame information.
369    */
getFrame(String clearClassName, String obfuscatedMethodName, String obfuscatedSignature, String obfuscatedFilename, int obfuscatedLine)370   public Frame getFrame(String clearClassName, String obfuscatedMethodName,
371       String obfuscatedSignature, String obfuscatedFilename, int obfuscatedLine) {
372     String clearSignature = getSignature(obfuscatedSignature);
373     ClassData classData = mClassesFromClearName.get(clearClassName);
374     if (classData == null) {
375       return new Frame(obfuscatedMethodName, clearSignature,
376           obfuscatedFilename, obfuscatedLine);
377     }
378     return classData.getFrame(clearClassName, obfuscatedMethodName, clearSignature,
379         obfuscatedFilename, obfuscatedLine);
380   }
381 
382   // Converts a proguard-formatted method signature into a Java formatted
383   // method signature.
fromProguardSignature(String sig)384   private static String fromProguardSignature(String sig) throws ParseException {
385     if (sig.startsWith("(")) {
386       int end = sig.indexOf(')');
387       if (end == -1) {
388         parseException("Error parsing signature: " + sig);
389       }
390 
391       StringBuilder converted = new StringBuilder();
392       converted.append('(');
393       if (end > 1) {
394         for (String arg : sig.substring(1, end).split(",")) {
395           converted.append(fromProguardSignature(arg));
396         }
397       }
398       converted.append(')');
399       converted.append(fromProguardSignature(sig.substring(end + 1)));
400       return converted.toString();
401     } else if (sig.endsWith(ARRAY_SYMBOL)) {
402       return "[" + fromProguardSignature(sig.substring(0, sig.length() - 2));
403     } else if (sig.equals("boolean")) {
404       return "Z";
405     } else if (sig.equals("byte")) {
406       return "B";
407     } else if (sig.equals("char")) {
408       return "C";
409     } else if (sig.equals("short")) {
410       return "S";
411     } else if (sig.equals("int")) {
412       return "I";
413     } else if (sig.equals("long")) {
414       return "J";
415     } else if (sig.equals("float")) {
416       return "F";
417     } else if (sig.equals("double")) {
418       return "D";
419     } else if (sig.equals("void")) {
420       return "V";
421     } else {
422       return "L" + sig.replace('.', '/') + ";";
423     }
424   }
425 
426   // Return a clear signature for the given obfuscated signature.
getSignature(String obfuscatedSig)427   private String getSignature(String obfuscatedSig) {
428     StringBuilder builder = new StringBuilder();
429     for (int i = 0; i < obfuscatedSig.length(); i++) {
430       if (obfuscatedSig.charAt(i) == 'L') {
431         int e = obfuscatedSig.indexOf(';', i);
432         builder.append('L');
433         String cls = obfuscatedSig.substring(i + 1, e).replace('/', '.');
434         builder.append(getClassName(cls).replace('.', '/'));
435         builder.append(';');
436         i = e;
437       } else {
438         builder.append(obfuscatedSig.charAt(i));
439       }
440     }
441     return builder.toString();
442   }
443 
444   // Return a file name for the given clear class name.
getFileName(String clearClass)445   private static String getFileName(String clearClass) {
446     String filename = clearClass;
447     int dot = filename.lastIndexOf('.');
448     if (dot != -1) {
449       filename = filename.substring(dot + 1);
450     }
451 
452     int dollar = filename.indexOf('$');
453     if (dollar != -1) {
454       filename = filename.substring(0, dollar);
455     }
456     return filename + ".java";
457   }
458 }
459