Fix ProguardMap line mapping

With R8 v3.1.4 the Proguard map stores line numbers slightly
differently.

Previously an entry having only one "clear" line of the form:
  <Start>:<End>:<Method info>:<OrigLine> -> <New name>
would map a line between Start and End to
  OrigLine + OffsetFromStart

In the new version, any line between Start and End gets mapped
precisely to OrigLine.

The ahat ProguardMap has been updated to check the version of the
map file and apply the correct mapping based on that.

Bug: 228000954
Test: m -j32 ahat ahat-tests && java -jar out/host/linux-x86/framework/ahat-tests.jar
Change-Id: I3a0ad58ccee1aae009d62759e59210a13530fc38
diff --git a/tools/ahat/src/main/com/android/ahat/proguard/ProguardMap.java b/tools/ahat/src/main/com/android/ahat/proguard/ProguardMap.java
index 699c417..cab8ec4 100644
--- a/tools/ahat/src/main/com/android/ahat/proguard/ProguardMap.java
+++ b/tools/ahat/src/main/com/android/ahat/proguard/ProguardMap.java
@@ -26,6 +26,8 @@
 import java.util.HashMap;
 import java.util.Map;
 import java.util.TreeMap;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
 
 /**
  * A representation of a proguard mapping for deobfuscating class names,
@@ -34,6 +36,7 @@
 public class ProguardMap {
 
   private static final String ARRAY_SYMBOL = "[]";
+  private static final Version LINE_MAPPING_BEHAVIOR_CHANGE_VERSION = new Version(3, 1, 4);
 
   private static class FrameData {
     public FrameData(String clearMethodName) {
@@ -41,31 +44,55 @@
     }
 
     private final String clearMethodName;
-    private final TreeMap<Integer, LineNumber> lineNumbers = new TreeMap<>();
+    private final TreeMap<Integer, LineNumberMapping> lineNumbers = new TreeMap<>();
 
     public int getClearLine(int obfuscatedLine) {
-      Map.Entry<Integer, LineNumber> lineNumberEntry = lineNumbers.floorEntry(obfuscatedLine);
-      LineNumber lineNumber = lineNumberEntry == null ? null : lineNumberEntry.getValue();
-      if (lineNumber != null
-          && obfuscatedLine >= lineNumber.obfuscatedLineStart
-          && obfuscatedLine <= lineNumber.obfuscatedLineEnd) {
-        return lineNumber.clearLineStart + obfuscatedLine - lineNumber.obfuscatedLineStart;
+      var lineNumberEntry = lineNumbers.floorEntry(obfuscatedLine);
+      LineNumberMapping mapping = lineNumberEntry == null ? null : lineNumberEntry.getValue();
+      if (mapping != null && mapping.hasObfuscatedLine(obfuscatedLine)) {
+        return mapping.mapObfuscatedLine(obfuscatedLine);
       } else {
         return obfuscatedLine;
       }
     }
   }
 
-  private static class LineNumber {
-    public LineNumber(int obfuscatedLineStart, int obfuscatedLineEnd, int clearLineStart) {
-      this.obfuscatedLineStart = obfuscatedLineStart;
-      this.obfuscatedLineEnd = obfuscatedLineEnd;
-      this.clearLineStart = clearLineStart;
+  private static class LineRange {
+    public LineRange(int start, int end) {
+      this.start = start;
+      this.end = end;
     }
 
-    private final int obfuscatedLineStart;
-    private final int obfuscatedLineEnd;
-    private final int clearLineStart;
+    public boolean hasLine(int lineNumber) {
+      return (lineNumber >= start && lineNumber <= end);
+    }
+
+    public final int start;
+    public final int end;
+  }
+
+  private static class LineNumberMapping {
+    public LineNumberMapping(LineRange obfuscatedRange, LineRange clearRange) {
+      this.obfuscatedRange = obfuscatedRange;
+      this.clearRange = clearRange;
+    }
+
+    public boolean hasObfuscatedLine(int lineNumber) {
+      return obfuscatedRange.hasLine(lineNumber);
+    }
+
+    public int mapObfuscatedLine(int lineNumber) {
+      int mappedLine = clearRange.start + lineNumber - obfuscatedRange.start;
+      if (!clearRange.hasLine(mappedLine)) {
+        // If the mapped line ends out outside of range, it would be past the end, so just limit it
+        // to the end line
+        return clearRange.end;
+      }
+      return mappedLine;
+    }
+
+    public final LineRange obfuscatedRange;
+    public final LineRange clearRange;
   }
 
   private static class ClassData {
@@ -102,14 +129,14 @@
     }
 
     public void addFrame(String obfuscatedMethodName, String clearMethodName,
-            String clearSignature, int obfuscatedLine, int obfuscatedLineEnd, int clearLine) {
+            String clearSignature, LineRange obfuscatedLine, LineRange clearRange) {
         String key = obfuscatedMethodName + clearSignature;
         FrameData data = mFrames.get(key);
         if (data == null) {
           data = new FrameData(clearMethodName);
         }
         data.lineNumbers.put(
-            obfuscatedLine, new LineNumber(obfuscatedLine, obfuscatedLineEnd, clearLine));
+            obfuscatedLine.start, new LineNumberMapping(obfuscatedLine, clearRange));
         mFrames.put(key, data);
     }
 
@@ -208,11 +235,13 @@
    *                        formatted proguard mapping file.
    */
   public void readFromReader(Reader mapReader) throws IOException, ParseException {
+    Version compilerVersion = new Version(0, 0, 0);
     BufferedReader reader = new BufferedReader(mapReader);
     String line = reader.readLine();
     while (line != null) {
       // Skip comment lines.
       if (isCommentLine(line)) {
+        compilerVersion = tryParseVersion(line, compilerVersion);
         line = reader.readLine();
         continue;
       }
@@ -258,14 +287,14 @@
           classData.addField(obfuscatedName, clearName);
         } else {
           // For methods, the type is of the form: [#:[#:]]<returnType>
-          int obfuscatedLine = 0;
+          int obfuscatedLineStart = 0;
           // The end of the obfuscated line range.
           // If line does not contain explicit end range, e.g #:, it is equivalent to #:#:
           int obfuscatedLineEnd = 0;
           int colon = type.indexOf(':');
           if (colon != -1) {
-            obfuscatedLine = Integer.parseInt(type.substring(0, colon));
-            obfuscatedLineEnd = obfuscatedLine;
+            obfuscatedLineStart = Integer.parseInt(type.substring(0, colon));
+            obfuscatedLineEnd = obfuscatedLineStart;
             type = type.substring(colon + 1);
           }
           colon = type.indexOf(':');
@@ -273,6 +302,7 @@
             obfuscatedLineEnd = Integer.parseInt(type.substring(0, colon));
             type = type.substring(colon + 1);
           }
+          LineRange obfuscatedRange = new LineRange(obfuscatedLineStart, obfuscatedLineEnd);
 
           // For methods, the clearName is of the form: <clearName><sig>[:#[:#]]
           int op = clearName.indexOf('(');
@@ -283,24 +313,35 @@
 
           String sig = clearName.substring(op, cp + 1);
 
-          int clearLine = obfuscatedLine;
+          int clearLineStart = obfuscatedRange.start;
+          int clearLineEnd = obfuscatedRange.end;
           colon = clearName.lastIndexOf(':');
           if (colon != -1) {
-            clearLine = Integer.parseInt(clearName.substring(colon + 1));
+            if (compilerVersion.compareTo(LINE_MAPPING_BEHAVIOR_CHANGE_VERSION) < 0) {
+              // Before v3.1.4 if only one clear line was present, that implied a range equal to the
+              // obfuscated line range
+              clearLineStart = Integer.parseInt(clearName.substring(colon + 1));
+              clearLineEnd = clearLineStart + obfuscatedRange.end - obfuscatedRange.start;
+            } else {
+              // From v3.1.4 if only one clear line was present, that implies that all lines map to
+              // a single clear line
+              clearLineEnd = Integer.parseInt(clearName.substring(colon + 1));
+              clearLineStart = clearLineEnd;
+            }
             clearName = clearName.substring(0, colon);
           }
 
           colon = clearName.lastIndexOf(':');
           if (colon != -1) {
-            clearLine = Integer.parseInt(clearName.substring(colon + 1));
+            clearLineStart = Integer.parseInt(clearName.substring(colon + 1));
             clearName = clearName.substring(0, colon);
           }
+          LineRange clearRange = new LineRange(clearLineStart, clearLineEnd);
 
           clearName = clearName.substring(0, op);
 
           String clearSig = fromProguardSignature(sig + type);
-          classData.addFrame(obfuscatedName, clearName, clearSig,
-                  obfuscatedLine, obfuscatedLineEnd, clearLine);
+          classData.addFrame(obfuscatedName, clearName, clearSig, obfuscatedRange, clearRange);
         }
 
         line = reader.readLine();
@@ -309,9 +350,49 @@
     reader.close();
   }
 
+  private static class Version implements Comparable<Version> {
+    final int major;
+    final int minor;
+    final int build;
+
+    public Version(int major, int minor, int build) {
+      this.major = major;
+      this.minor = minor;
+      this.build = build;
+    }
+
+    @Override
+    public int compareTo(Version other) {
+      int compare = Integer.compare(this.major, other.major);
+      if (compare == 0) {
+        compare = Integer.compare(this.minor, other.minor);
+      }
+      if (compare == 0) {
+        compare = Integer.compare(this.build, other.build);
+      }
+      return compare;
+    }
+  }
+
   private boolean isCommentLine(String line) {
-      // Comment lines start with '#' and my have leading whitespaces.
-      return line.trim().startsWith("#");
+    // Comment lines start with '#' and my have leading whitespaces.
+    return line.trim().startsWith("#");
+  }
+
+  private Version tryParseVersion(String line, Version old) {
+    Pattern pattern = Pattern.compile("#\\s*compiler_version:\\s*(\\d+).(\\d+).(?:(\\d+))?");
+    Matcher matcher = pattern.matcher(line);
+    if (matcher.find()) {
+      String buildStr = matcher.group(3);
+      if (buildStr == null) {
+        buildStr = Integer.toString(0);
+      }
+      return new Version(
+          Integer.parseInt(matcher.group(1)),
+          Integer.parseInt(matcher.group(2)),
+          Integer.parseInt(buildStr));
+    }
+    return old;
   }
 
   /**
diff --git a/tools/ahat/src/test/com/android/ahat/ProguardMapTest.java b/tools/ahat/src/test/com/android/ahat/ProguardMapTest.java
index a569fd4..f0e2baf 100644
--- a/tools/ahat/src/test/com/android/ahat/ProguardMapTest.java
+++ b/tools/ahat/src/test/com/android/ahat/ProguardMapTest.java
@@ -24,9 +24,9 @@
 import static org.junit.Assert.assertEquals;
 
 public class ProguardMapTest {
-  private static final String TEST_MAP =
+  private static final String TEST_MAP_FORMAT =
       "# compiler: richard\n"
-    + "# compiler_version: 3.0-dev\n"
+    + "# compiler_version: %s-dev\n"
     + "# min_api: 10000\n"
     + "# compiler_hash: b7e25308967a577aa1f05a4b5a745c26\n"
     + "  # indented comment\n"
@@ -56,7 +56,12 @@
     ;
 
   @Test
-  public void proguardMap() throws IOException, ParseException {
+  public void oldProguardMap() throws IOException, ParseException {
+      runOldProguardMap(String.format(TEST_MAP_FORMAT, "3.0.1"));
+      runOldProguardMap(String.format(TEST_MAP_FORMAT, "3.1"));
+  }
+
+  public void runOldProguardMap(String testMap) throws IOException, ParseException {
     ProguardMap map = new ProguardMap();
 
     // An empty proguard map should not deobfuscate anything.
@@ -72,7 +77,7 @@
     assertEquals(123, frame.line);
 
     // Read in the proguard map.
-    map.readFromReader(new StringReader(TEST_MAP));
+    map.readFromReader(new StringReader(testMap));
 
     // It should still not deobfuscate things that aren't in the map
     assertEquals("foo.bar.Sludge", map.getClassName("foo.bar.Sludge"));
@@ -179,4 +184,134 @@
         "()V", "SourceFile.java", 0);
     assertEquals("Methods.java", frame.filename);
   }
+
+  @Test
+  public void proguardMap() throws IOException, ParseException {
+      runNewProguardMap(String.format(TEST_MAP_FORMAT, "3.1.4"));
+      runNewProguardMap(String.format(TEST_MAP_FORMAT, "3.2"));
+  }
+
+  public void runNewProguardMap(String testMap) throws IOException, ParseException {
+    ProguardMap map = new ProguardMap();
+
+    // An empty proguard map should not deobfuscate anything.
+    assertEquals("foo.bar.Sludge", map.getClassName("foo.bar.Sludge"));
+    assertEquals("fooBarSludge", map.getClassName("fooBarSludge"));
+    assertEquals("myfield", map.getFieldName("foo.bar.Sludge", "myfield"));
+    assertEquals("myfield", map.getFieldName("fooBarSludge", "myfield"));
+    ProguardMap.Frame frame = map.getFrame(
+        "foo.bar.Sludge", "mymethod", "(Lfoo/bar/Sludge;)V", "SourceFile.java", 123);
+    assertEquals("mymethod", frame.method);
+    assertEquals("(Lfoo/bar/Sludge;)V", frame.signature);
+    assertEquals("SourceFile.java", frame.filename);
+    assertEquals(123, frame.line);
+
+    // Read in the proguard map.
+    map.readFromReader(new StringReader(testMap));
+
+    // It should still not deobfuscate things that aren't in the map
+    assertEquals("foo.bar.Sludge", map.getClassName("foo.bar.Sludge"));
+    assertEquals("fooBarSludge", map.getClassName("fooBarSludge"));
+    assertEquals("myfield", map.getFieldName("foo.bar.Sludge", "myfield"));
+    assertEquals("myfield", map.getFieldName("fooBarSludge", "myfield"));
+    frame = map.getFrame("foo.bar.Sludge", "mymethod", "(Lfoo/bar/Sludge;)V",
+        "SourceFile.java", 123);
+    assertEquals("mymethod", frame.method);
+    assertEquals("(Lfoo/bar/Sludge;)V", frame.signature);
+    assertEquals("SourceFile.java", frame.filename);
+    assertEquals(123, frame.line);
+
+    // Test deobfuscation of class names
+    assertEquals("class.that.is.Empty", map.getClassName("a"));
+    assertEquals("class.that.is.Empty$subclass", map.getClassName("b"));
+    assertEquals("class.with.only.Fields", map.getClassName("c"));
+    assertEquals("class.with.Methods", map.getClassName("d"));
+
+    // Test deobfuscation of array classes.
+    assertEquals("class.with.Methods[]", map.getClassName("d[]"));
+    assertEquals("class.with.Methods[][]", map.getClassName("d[][]"));
+
+    // Test deobfuscation of fields
+    assertEquals("prim_type_field", map.getFieldName("class.with.only.Fields", "a"));
+    assertEquals("prim_array_type_field", map.getFieldName("class.with.only.Fields", "b"));
+    assertEquals("class_type_field", map.getFieldName("class.with.only.Fields", "c"));
+    assertEquals("array_type_field", map.getFieldName("class.with.only.Fields", "d"));
+    assertEquals("longObfuscatedNameField", map.getFieldName("class.with.only.Fields", "abc"));
+    assertEquals("some_field", map.getFieldName("class.with.Methods", "a"));
+
+    // Test deobfuscation of frames
+    frame = map.getFrame("class.with.Methods", "<clinit>", "()V", "SourceFile.java", 13);
+    assertEquals("<clinit>", frame.method);
+    assertEquals("()V", frame.signature);
+    assertEquals("Methods.java", frame.filename);
+    assertEquals(13, frame.line);
+
+    frame = map.getFrame("class.with.Methods", "m", "()V", "SourceFile.java", 42);
+    assertEquals("boringMethod", frame.method);
+    assertEquals("()V", frame.signature);
+    assertEquals("Methods.java", frame.filename);
+    assertEquals(42, frame.line);
+
+    frame = map.getFrame("class.with.Methods", "m", "(IF)V", "SourceFile.java", 45);
+    assertEquals("methodWithPrimArgs", frame.method);
+    assertEquals("(IF)V", frame.signature);
+    assertEquals("Methods.java", frame.filename);
+    assertEquals(45, frame.line);
+
+    frame = map.getFrame("class.with.Methods", "m", "([IF)V", "SourceFile.java", 49);
+    assertEquals("methodWithPrimArrArgs", frame.method);
+    assertEquals("([IF)V", frame.signature);
+    assertEquals("Methods.java", frame.filename);
+    assertEquals(49, frame.line);
+
+    frame = map.getFrame("class.with.Methods", "m", "(Lclass/not/in/Map;)V",
+        "SourceFile.java", 52);
+    assertEquals("methodWithClearObjArg", frame.method);
+    assertEquals("(Lclass/not/in/Map;)V", frame.signature);
+    assertEquals("Methods.java", frame.filename);
+    assertEquals(52, frame.line);
+
+    frame = map.getFrame("class.with.Methods", "m", "([Lclass/not/in/Map;)V",
+        "SourceFile.java", 57);
+    assertEquals("methodWithClearObjArrArg", frame.method);
+    assertEquals("([Lclass/not/in/Map;)V", frame.signature);
+    assertEquals("Methods.java", frame.filename);
+    assertEquals(57, frame.line);
+
+    frame = map.getFrame("class.with.Methods", "m", "(Lc;)V", "SourceFile.java", 59);
+    assertEquals("methodWithObfObjArg", frame.method);
+    assertEquals("(Lclass/with/only/Fields;)V", frame.signature);
+    assertEquals("Methods.java", frame.filename);
+    assertEquals(59, frame.line);
+
+    frame = map.getFrame("class.with.Methods", "n", "()Lc;", "SourceFile.java", 64);
+    assertEquals("methodWithObfRes", frame.method);
+    assertEquals("()Lclass/with/only/Fields;", frame.signature);
+    assertEquals("Methods.java", frame.filename);
+    assertEquals(64, frame.line);
+
+    frame = map.getFrame("class.with.Methods", "o", "()V", "SourceFile.java", 80);
+    assertEquals("lineObfuscatedMethod", frame.method);
+    assertEquals("()V", frame.signature);
+    assertEquals("Methods.java", frame.filename);
+    assertEquals(8, frame.line);
+
+    frame = map.getFrame("class.with.Methods", "o", "()V", "SourceFile.java", 103);
+    assertEquals("lineObfuscatedMethod", frame.method);
+    assertEquals("()V", frame.signature);
+    assertEquals("Methods.java", frame.filename);
+    assertEquals(50, frame.line);
+
+    frame = map.getFrame("class.with.Methods", "p", "()V", "SourceFile.java", 94);
+    assertEquals("lineObfuscatedMethod2", frame.method);
+    assertEquals("()V", frame.signature);
+    assertEquals("Methods.java", frame.filename);
+    assertEquals(9, frame.line);
+
+    // Some methods may not have been obfuscated. We should still be able
+    // to compute the filename properly.
+    frame = map.getFrame("class.with.Methods", "unObfuscatedMethodName",
+        "()V", "SourceFile.java", 0);
+    assertEquals("Methods.java", frame.filename);
+  }
 }