Home | History | Annotate | Download | only in proguard
      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 {
     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 
     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 {
     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.
     81     public ClassData(String clearName) {
     82       mClearName = clearName;
     83     }
     84 
     85     // Returns the clear name of the class.
     86     public String getClearName() {
     87       return mClearName;
     88     }
     89 
     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?
     99     public String getField(String obfuscatedName) {
    100       String clearField = mFields.get(obfuscatedName);
    101       return clearField == null ? obfuscatedName : clearField;
    102     }
    103 
    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 
    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 {
    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 
    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    */
    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    */
    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    */
    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    */
    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    */
    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    */
    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.
    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.
    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.
    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