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