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
17package com.android.ahat.proguard;
18
19import java.io.BufferedReader;
20import java.io.File;
21import java.io.FileNotFoundException;
22import java.io.FileReader;
23import java.io.IOException;
24import java.io.Reader;
25import java.text.ParseException;
26import java.util.HashMap;
27import java.util.Map;
28
29/**
30 * A representation of a proguard mapping for deobfuscating class names,
31 * field names, and stack frames.
32 */
33public class ProguardMap {
34
35  private static final String ARRAY_SYMBOL = "[]";
36
37  private static class FrameData {
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.
57    public ClassData(String clearName) {
58      mClearName = clearName;
59    }
60
61    // Returns the clear name of the class.
62    public String getClearName() {
63      return mClearName;
64    }
65
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?
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?
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
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 {
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
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   */
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   */
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   */
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   */
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   */
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   */
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.
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.
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.
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