1 // Copyright (c) 2016, the R8 project authors. Please see the AUTHORS file 2 // for details. All rights reserved. Use of this source code is governed by a 3 // BSD-style license that can be found in the LICENSE file. 4 package com.android.tools.r8.naming; 5 6 import com.android.tools.r8.logging.Log; 7 import com.android.tools.r8.naming.MemberNaming.FieldSignature; 8 import com.android.tools.r8.naming.MemberNaming.MethodSignature; 9 import com.android.tools.r8.naming.MemberNaming.Range; 10 import com.android.tools.r8.naming.MemberNaming.Signature; 11 import com.android.tools.r8.naming.MemberNaming.SingleLineRange; 12 import com.google.common.collect.ImmutableMap; 13 import java.io.BufferedReader; 14 import java.io.ByteArrayInputStream; 15 import java.io.IOException; 16 import java.io.InputStream; 17 import java.io.InputStreamReader; 18 import java.nio.charset.StandardCharsets; 19 import java.nio.file.Files; 20 import java.nio.file.Path; 21 import java.util.ArrayList; 22 import java.util.HashMap; 23 import java.util.LinkedList; 24 import java.util.List; 25 import java.util.Map; 26 import java.util.function.Consumer; 27 28 /** 29 * Parses a Proguard mapping file and produces mappings from obfuscated class names to the original 30 * name and from obfuscated member signatures to the original members the obfuscated member 31 * was formed of. 32 * <p> 33 * The expected format is as follows 34 * <p> 35 * original-type-name ARROW obfuscated-type-name COLON starts a class mapping 36 * description and maps original to obfuscated. 37 * <p> 38 * followed by one or more of 39 * <p> 40 * signature ARROW name 41 * <p> 42 * which maps the member with the given signature to the new name. This mapping is not 43 * bidirectional as member names are overloaded by signature. To make it bidirectional, we extend 44 * the name with the signature of the original member. 45 * <p> 46 * Due to inlining, we might have the above prefixed with a range (two numbers separated by :). 47 * <p> 48 * range COLON signature ARROW name 49 * <p> 50 * This has the same meaning as the above but also encodes the line number range of the member. This 51 * may be followed by multiple inline mappings of the form 52 * <p> 53 * range COLON signature COLON range ARROW name 54 * <p> 55 * to identify that signature was inlined from the second range to the new line numbers in the first 56 * range. This is then followed by information on the call trace to where the member was inlined. 57 * These entries have the form 58 * <p> 59 * range COLON signature COLON number ARROW name 60 * <p> 61 * and are currently only stored to be able to reproduce them later. 62 */ 63 public class ProguardMapReader implements AutoCloseable { 64 65 private final BufferedReader reader; 66 close()67 public void close() throws IOException { 68 if (reader != null) { 69 reader.close(); 70 } 71 } 72 ProguardMapReader(BufferedReader reader)73 private ProguardMapReader(BufferedReader reader) throws IOException { 74 this.reader = reader; 75 } 76 mapperFromInputStream(InputStream in)77 public static ClassNameMapper mapperFromInputStream(InputStream in) throws IOException { 78 BufferedReader reader = new BufferedReader(new InputStreamReader(in, "UTF8")); 79 try (ProguardMapReader proguardReader = new ProguardMapReader(reader)) { 80 return proguardReader.parse(); 81 } 82 } 83 mapperFromFile(Path path)84 public static ClassNameMapper mapperFromFile(Path path) throws IOException { 85 return mapperFromInputStream(Files.newInputStream(path)); 86 } 87 mapperFromString(String contents)88 public static ClassNameMapper mapperFromString(String contents) throws IOException { 89 return mapperFromInputStream( 90 new ByteArrayInputStream(contents.getBytes(StandardCharsets.UTF_8))); 91 } 92 93 // Internal parser state 94 private int lineNo = 0; 95 private int lineOffset = 0; 96 private String line; 97 peek()98 private char peek() { 99 return peek(0); 100 } 101 peek(int distance)102 private char peek(int distance) { 103 return lineOffset + distance < line.length() 104 ? line.charAt(lineOffset + distance) 105 : '\n'; 106 } 107 next()108 private char next() { 109 try { 110 return line.charAt(lineOffset++); 111 } catch (ArrayIndexOutOfBoundsException e) { 112 throw new ParseException("Unexpected end of line"); 113 } 114 } 115 nextLine()116 private boolean nextLine() throws IOException { 117 if (line.length() != lineOffset) { 118 throw new ParseException("Expected end of line"); 119 } 120 return skipLine(); 121 } 122 skipLine()123 private boolean skipLine() throws IOException { 124 lineNo++; 125 lineOffset = 0; 126 line = reader.readLine(); 127 return hasLine(); 128 } 129 hasLine()130 private boolean hasLine() { 131 return line != null; 132 } 133 134 // Helpers for common pattern skipWhitespace()135 private void skipWhitespace() { 136 while (Character.isWhitespace(peek())) { 137 next(); 138 } 139 } 140 expect(char c)141 private char expect(char c) { 142 if (next() != c) { 143 throw new ParseException("Expected '" + c + "'"); 144 } 145 return c; 146 } 147 parse()148 public ClassNameMapper parse() throws IOException { 149 // Read the first line. 150 line = reader.readLine(); 151 Map<String, ClassNaming> classNames = parseClassMappings(); 152 return new ClassNameMapper(classNames); 153 } 154 155 // Parsing of entries 156 parseClassMappings()157 private Map<String, ClassNaming> parseClassMappings() throws IOException { 158 ImmutableMap.Builder<String, ClassNaming> builder = ImmutableMap.builder(); 159 while (hasLine()) { 160 String before = parseType(false); 161 skipWhitespace(); 162 // Workaround for proguard map files that contain entries for package-info.java files. 163 if (!acceptArrow()) { 164 // If this was a package-info line, we parsed the "package" string. 165 if (!before.endsWith("package") || !acceptString("-info")) { 166 throw new ParseException("Expected arrow after class name " + before); 167 } 168 skipLine(); 169 continue; 170 } 171 skipWhitespace(); 172 String after = parseType(false); 173 expect(':'); 174 ClassNaming currentClass = new ClassNaming(after, before); 175 builder.put(after, currentClass); 176 if (nextLine()) { 177 parseMemberMappings(currentClass); 178 } 179 } 180 return builder.build(); 181 } 182 parseMemberMappings(ClassNaming currentClass)183 private void parseMemberMappings(ClassNaming currentClass) throws IOException { 184 MemberNaming current = null; 185 Range previousInlineRange = null; 186 Signature previousSignature = null; 187 String previousRenamedName = null; 188 List<Consumer<MemberNaming>> collectedInfos = new ArrayList<>(10); 189 190 while (Character.isWhitespace(peek())) { 191 skipWhitespace(); 192 Range inlinedLineRange = maybeParseRange(); 193 if (inlinedLineRange != null) { 194 expect(':'); 195 } 196 Signature signature = parseSignature(); 197 Range originalLineRange; 198 if (peek() == ':') { 199 // This is an inlining definition 200 next(); 201 originalLineRange = maybeParseRange(); 202 if (originalLineRange == null) { 203 if (!skipLine()) { 204 break; 205 } 206 continue; 207 } 208 } else { 209 originalLineRange = null; 210 } 211 skipWhitespace(); 212 skipArrow(); 213 skipWhitespace(); 214 String renamedName = parseMethodName(); 215 // If there is no line number information at the front or if it changes, we have a new 216 // segment. Likewise, if the range information on the right hand side has two values, we have 217 // a new segment. 218 if (inlinedLineRange == null 219 || previousInlineRange == null 220 || originalLineRange == null 221 || !previousInlineRange.equals(inlinedLineRange) 222 || !originalLineRange.isSingle()) { 223 // We are at a range boundary. Either we parsed something new, or an inline frame is over. 224 // We detect this by checking whether the previous signature matches the one of current. 225 if (current == null || !previousSignature.equals(current.signature)) { 226 if (collectedInfos.size() == 1) { 227 current = new MemberNaming(previousSignature, previousRenamedName, previousInlineRange); 228 currentClass.addMemberEntry(current); 229 } else { 230 if (Log.ENABLED && !collectedInfos.isEmpty()) { 231 Log.warn(getClass(), 232 "More than one member entry that forms a new group at %s %s -> %s", 233 previousInlineRange, previousSignature, previousRenamedName); 234 } 235 } 236 } else { 237 MemberNaming finalCurrent = current; 238 collectedInfos.forEach(info -> info.accept(finalCurrent)); 239 } 240 collectedInfos.clear(); 241 } 242 // Defer the creation of the info until we have the correct member. 243 collectedInfos.add((m) -> m.addInliningRange(inlinedLineRange, signature, originalLineRange)); 244 // We have parsed the whole line, move on. 245 previousInlineRange = inlinedLineRange; 246 previousSignature = signature; 247 previousRenamedName = renamedName; 248 if (!nextLine()) { 249 break; 250 } 251 } 252 // Process the last round if lines have been read. 253 if (current == null || !previousSignature.equals(current.signature)) { 254 if (collectedInfos.size() == 1) { 255 current = new MemberNaming(previousSignature, previousRenamedName, previousInlineRange); 256 currentClass.addMemberEntry(current); 257 } 258 } else { 259 MemberNaming finalCurrent = current; 260 collectedInfos.forEach(info -> info.accept(finalCurrent)); 261 } 262 collectedInfos.clear(); 263 } 264 265 // Parsing of components 266 skipIdentifier(boolean allowInit)267 private void skipIdentifier(boolean allowInit) { 268 boolean isInit = false; 269 if (allowInit && peek() == '<') { 270 // swallow the leading < character 271 next(); 272 isInit = true; 273 } 274 if (!Character.isJavaIdentifierStart(peek())) { 275 throw new ParseException("Identifier expected"); 276 } 277 next(); 278 while (Character.isJavaIdentifierPart(peek())) { 279 next(); 280 } 281 if (isInit) { 282 expect('>'); 283 } 284 if (Character.isJavaIdentifierPart(peek())) { 285 throw new ParseException("End of identifier expected"); 286 } 287 } 288 289 // Cache for canonicalizing strings. 290 // This saves 10% of heap space for large programs. 291 final HashMap<String, String> cache = new HashMap<>(); 292 substring(int start)293 private String substring(int start) { 294 String result = line.substring(start, lineOffset); 295 if (cache.containsKey(result)) { 296 return cache.get(result); 297 } 298 cache.put(result, result); 299 return result; 300 } 301 parseMethodName()302 private String parseMethodName() { 303 int startPosition = lineOffset; 304 skipIdentifier(true); 305 while (peek() == '.') { 306 next(); 307 skipIdentifier(true); 308 } 309 return substring(startPosition); 310 } 311 parseType(boolean allowArray)312 private String parseType(boolean allowArray) { 313 int startPosition = lineOffset; 314 skipIdentifier(false); 315 while (peek() == '.') { 316 next(); 317 skipIdentifier(false); 318 } 319 if (allowArray) { 320 while (peek() == '[') { 321 next(); 322 expect(']'); 323 } 324 } 325 return substring(startPosition); 326 } 327 parseSignature()328 private Signature parseSignature() { 329 String type = parseType(true); 330 expect(' '); 331 String name = parseMethodName(); 332 Signature signature; 333 if (peek() == '(') { 334 next(); 335 String[] arguments; 336 if (peek() == ')') { 337 arguments = new String[0]; 338 } else { 339 List<String> items = new LinkedList<>(); 340 items.add(parseType(true)); 341 while (peek() != ')') { 342 expect(','); 343 items.add(parseType(true)); 344 } 345 arguments = items.toArray(new String[items.size()]); 346 } 347 expect(')'); 348 signature = new MethodSignature(name, type, arguments); 349 } else { 350 signature = new FieldSignature(name, type); 351 } 352 return signature; 353 } 354 skipArrow()355 private void skipArrow() { 356 expect('-'); 357 expect('>'); 358 } 359 acceptArrow()360 private boolean acceptArrow() { 361 if (peek() == '-' && peek(1) == '>') { 362 next(); 363 next(); 364 return true; 365 } 366 return false; 367 } 368 acceptString(String s)369 private boolean acceptString(String s) { 370 for (int i = 0; i < s.length(); i++) { 371 if (peek(i) != s.charAt(i)) { 372 return false; 373 } 374 } 375 for (int i = 0; i < s.length(); i++) { 376 next(); 377 } 378 return true; 379 } 380 maybeParseRange()381 private Range maybeParseRange() { 382 if (!Character.isDigit(peek())) { 383 return null; 384 } 385 int from = parseNumber(); 386 if (peek() != ':') { 387 return new SingleLineRange(from); 388 } 389 expect(':'); 390 int to = parseNumber(); 391 return new Range(from, to); 392 } 393 parseNumber()394 private int parseNumber() { 395 int result = 0; 396 if (!Character.isDigit(peek())) { 397 throw new ParseException("Number expected"); 398 } 399 do { 400 result *= 10; 401 result += Character.getNumericValue(next()); 402 } while (Character.isDigit(peek())); 403 return result; 404 } 405 406 private class ParseException extends RuntimeException { 407 408 private final int lineNo; 409 private final int lineOffset; 410 private final String msg; 411 ParseException(String msg)412 ParseException(String msg) { 413 lineNo = ProguardMapReader.this.lineNo; 414 lineOffset = ProguardMapReader.this.lineOffset; 415 this.msg = msg; 416 } 417 toString()418 public String toString() { 419 return "Parse error [" + lineNo + ":" + lineOffset + "] " + msg; 420 } 421 } 422 } 423