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 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 */ getClassName(String obfuscatedClassName)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 */ getFieldName(String clearClass, String obfuscatedField)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 */ getFrame(String clearClassName, String obfuscatedMethodName, String obfuscatedSignature, String obfuscatedFilename, int obfuscatedLine)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. fromProguardSignature(String sig)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. getSignature(String obfuscatedSig)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. getFileName(String clearClass)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