Initial entrypoint for retrace with basic tests of functionality

This CL establishes the main entrypoint for the retrace tool. This
will be a work in progress to get the actual retrace functionality in
place.

Bug: 132850880
Change-Id: I1ca5ec6d6c470ab34852814a1cae26720f4007d7
diff --git a/src/main/java/com/android/tools/r8/retrace/Retrace.java b/src/main/java/com/android/tools/r8/retrace/Retrace.java
new file mode 100644
index 0000000..834e843
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/retrace/Retrace.java
@@ -0,0 +1,175 @@
+// Copyright (c) 2019, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.retrace;
+
+import static com.android.tools.r8.utils.ExceptionUtils.STATUS_ERROR;
+
+import com.android.tools.r8.DiagnosticsHandler;
+import com.android.tools.r8.Keep;
+import com.android.tools.r8.naming.ClassNameMapper;
+import com.android.tools.r8.retrace.RetraceCommand.Builder;
+import com.android.tools.r8.retrace.RetraceCommand.ProguardMapProducer;
+import com.android.tools.r8.retrace.RetraceCore.RetraceResult;
+import com.android.tools.r8.utils.OptionsParsing;
+import com.android.tools.r8.utils.OptionsParsing.ParseContext;
+import com.android.tools.r8.utils.StringDiagnostic;
+import com.android.tools.r8.utils.StringUtils;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Scanner;
+
+/**
+ * A retrace tool for obfuscated stack traces.
+ *
+ * <p>This is the interface for getting de-obfuscating stack traces, similar to the proguard retrace
+ * tool.
+ */
+@Keep
+public class Retrace {
+
+  public static final String USAGE_MESSAGE =
+      StringUtils.lines(
+          "Usage: retrace [--verbose] <proguard-map> <stacktrace-file>",
+          "  where <proguard-map> is an r8 generated mapping file.");
+
+  private static Builder parseArguments(String[] args, DiagnosticsHandler diagnosticsHandler) {
+    ParseContext context = new ParseContext(args);
+    Builder builder = RetraceCommand.builder(diagnosticsHandler);
+    boolean hasSetProguardMap = false;
+    boolean hasSetStackTrace = false;
+    while (context.head() != null) {
+      Boolean help = OptionsParsing.tryParseBoolean(context, "--help");
+      if (help != null) {
+        return null;
+      }
+      Boolean verbose = OptionsParsing.tryParseBoolean(context, "--verbose");
+      if (verbose != null) {
+        // TODO(b/132850880): Enable support for verbose.
+        diagnosticsHandler.error(new StringDiagnostic("Currently no support for --verbose"));
+        continue;
+      }
+      if (!hasSetProguardMap) {
+        builder.setProguardMapProducer(getMappingSupplier(context.head(), diagnosticsHandler));
+        context.next();
+        hasSetProguardMap = true;
+      } else if (!hasSetStackTrace) {
+        builder.setStackTrace(getStackTraceFromFile(context.head(), diagnosticsHandler));
+        context.next();
+        hasSetStackTrace = true;
+      } else {
+        diagnosticsHandler.error(
+            new StringDiagnostic(
+                String.format("Too many arguments specified for builder at '%s'", context.head())));
+        diagnosticsHandler.error(new StringDiagnostic(USAGE_MESSAGE));
+        throw new RetraceAbortException();
+      }
+    }
+    if (!hasSetProguardMap) {
+      diagnosticsHandler.error(new StringDiagnostic("Mapping file not specified"));
+      throw new RetraceAbortException();
+    }
+    if (!hasSetStackTrace) {
+      builder.setStackTrace(getStackTraceFromStandardInput());
+    }
+    return builder;
+  }
+
+  private static ProguardMapProducer getMappingSupplier(
+      String mappingPath, DiagnosticsHandler diagnosticsHandler) {
+    Path path = Paths.get(mappingPath);
+    if (!Files.exists(path)) {
+      diagnosticsHandler.error(
+          new StringDiagnostic(String.format("Could not find mapping file '%s'.", mappingPath)));
+      throw new RetraceAbortException();
+    }
+    return () -> new String(Files.readAllBytes(path));
+  }
+
+  private static List<String> getStackTraceFromFile(
+      String stackTracePath, DiagnosticsHandler diagnostics) {
+    try {
+      return Files.readAllLines(Paths.get(stackTracePath));
+    } catch (IOException e) {
+      diagnostics.error(new StringDiagnostic("Could not find stack trace file: " + stackTracePath));
+      throw new RetraceAbortException();
+    }
+  }
+
+  /**
+   * The main entry point for running the retrace.
+   *
+   * @param command The command that describes the desired behavior of this retrace invocation.
+   */
+  public static void run(RetraceCommand command) {
+    try {
+      ClassNameMapper classNameMapper =
+          ClassNameMapper.mapperFromString(command.proguardMapProducer.get());
+      RetraceResult result =
+          new RetraceCore(classNameMapper, command.stackTrace, command.diagnosticsHandler)
+              .retrace();
+      command.retracedStackTraceConsumer.accept(result.toList());
+    } catch (IOException ex) {
+      command.diagnosticsHandler.error(
+          new StringDiagnostic("Could not open mapping input stream: " + ex.getMessage()));
+      throw new RetraceAbortException();
+    }
+  }
+
+  static void run(String[] args) {
+    DiagnosticsHandler diagnosticsHandler = new DiagnosticsHandler() {};
+    Builder builder = parseArguments(args, diagnosticsHandler);
+    if (builder == null) {
+      // --help was an argument to list
+      assert Arrays.asList(args).contains("--help");
+      System.out.print(USAGE_MESSAGE);
+      return;
+    }
+    builder.setRetracedStackTraceConsumer(
+        retraced -> System.out.print(StringUtils.lines(retraced)));
+    run(builder.build());
+  }
+  /**
+   * The main entry point for running a legacy compatible retrace from the command line.
+   *
+   * @param args The argument that describes this command.
+   */
+  public static void main(String... args) {
+    withMainProgramHandler(() -> run(args));
+  }
+
+  private static List<String> getStackTraceFromStandardInput() {
+    Scanner sc = new Scanner(System.in);
+    List<String> readLines = new ArrayList<>();
+    while (sc.hasNext()) {
+      readLines.add(sc.nextLine());
+    }
+    return readLines;
+  }
+
+  static class RetraceAbortException extends RuntimeException {}
+
+  private interface MainAction {
+    void run() throws RetraceAbortException;
+  }
+
+  private static void withMainProgramHandler(MainAction action) {
+    try {
+      action.run();
+    } catch (RetraceAbortException e) {
+      // Detail of the errors were already reported
+      System.exit(STATUS_ERROR);
+    } catch (RuntimeException e) {
+      System.err.println("Retrace failed with an internal error.");
+      Throwable cause = e.getCause() == null ? e : e.getCause();
+      cause.printStackTrace();
+      System.exit(STATUS_ERROR);
+    }
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/retrace/RetraceCommand.java b/src/main/java/com/android/tools/r8/retrace/RetraceCommand.java
new file mode 100644
index 0000000..5385b09
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/retrace/RetraceCommand.java
@@ -0,0 +1,128 @@
+// Copyright (c) 2019, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.retrace;
+
+import com.android.tools.r8.DiagnosticsHandler;
+import com.android.tools.r8.Keep;
+import java.io.IOException;
+import java.util.List;
+import java.util.function.Consumer;
+
+public class RetraceCommand {
+
+  final boolean isVerbose;
+  final DiagnosticsHandler diagnosticsHandler;
+  final ProguardMapProducer proguardMapProducer;
+  final List<String> stackTrace;
+  final Consumer<List<String>> retracedStackTraceConsumer;
+
+  private RetraceCommand(
+      boolean isVerbose,
+      DiagnosticsHandler diagnosticsHandler,
+      ProguardMapProducer proguardMapProducer,
+      List<String> stackTrace,
+      Consumer<List<String>> retracedStackTraceConsumer) {
+    this.isVerbose = isVerbose;
+    this.diagnosticsHandler = diagnosticsHandler;
+    this.proguardMapProducer = proguardMapProducer;
+    this.stackTrace = stackTrace;
+    this.retracedStackTraceConsumer = retracedStackTraceConsumer;
+
+    assert this.diagnosticsHandler != null;
+    assert this.proguardMapProducer != null;
+    assert this.stackTrace != null;
+    assert this.retracedStackTraceConsumer != null;
+  }
+
+  /**
+   * Utility method for obtaining a RetraceCommand builder.
+   *
+   * @param diagnosticsHandler The diagnostics handler for consuming messages.
+   */
+  public static Builder builder(DiagnosticsHandler diagnosticsHandler) {
+    return new Builder(diagnosticsHandler);
+  }
+
+  /** Utility method for obtaining a RetraceCommand builder with a default diagnostics handler. */
+  public static Builder builder() {
+    return new Builder(new DiagnosticsHandler() {});
+  }
+
+  public static class Builder {
+
+    private boolean isVerbose;
+    private DiagnosticsHandler diagnosticsHandler;
+    private ProguardMapProducer proguardMapProducer;
+    private List<String> stackTrace;
+    private Consumer<List<String>> retracedStackTraceConsumer;
+
+    private Builder(DiagnosticsHandler diagnosticsHandler) {
+      this.diagnosticsHandler = diagnosticsHandler;
+    }
+
+    /** Set if the produced stack trace should have additional information. */
+    public Builder isVerbose() {
+      this.isVerbose = true;
+      return this;
+    }
+
+    /**
+     * Set a producer for the proguard mapping contents.
+     *
+     * @param producer Producer for
+     */
+    public Builder setProguardMapProducer(ProguardMapProducer producer) {
+      this.proguardMapProducer = producer;
+      return this;
+    }
+
+    /**
+     * Set the obfuscated stack trace that is to be retraced.
+     *
+     * @param stackTrace Stack trace having the top entry(the closest stack to the error) as the
+     *     first line.
+     */
+    public Builder setStackTrace(List<String> stackTrace) {
+      this.stackTrace = stackTrace;
+      return this;
+    }
+
+    /**
+     * Set a consumer for receiving the retraced stack trace.
+     *
+     * @param consumer Consumer for receiving the retraced stack trace.
+     */
+    public Builder setRetracedStackTraceConsumer(Consumer<List<String>> consumer) {
+      this.retracedStackTraceConsumer = consumer;
+      return this;
+    }
+
+    public RetraceCommand build() {
+      if (this.diagnosticsHandler == null) {
+        throw new RuntimeException("DiagnosticsHandler not specified");
+      }
+      if (this.proguardMapProducer == null) {
+        throw new RuntimeException("ProguardMapSupplier not specified");
+      }
+      if (this.stackTrace == null) {
+        throw new RuntimeException("StackTrace not specified");
+      }
+      if (this.retracedStackTraceConsumer == null) {
+        throw new RuntimeException("RetracedStackConsumer not specified");
+      }
+      return new RetraceCommand(
+          isVerbose,
+          diagnosticsHandler,
+          proguardMapProducer,
+          stackTrace,
+          retracedStackTraceConsumer);
+    }
+  }
+
+  @Keep
+  public interface ProguardMapProducer {
+    String get() throws IOException;
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/retrace/RetraceCore.java b/src/main/java/com/android/tools/r8/retrace/RetraceCore.java
new file mode 100644
index 0000000..4eca518
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/retrace/RetraceCore.java
@@ -0,0 +1,84 @@
+// Copyright (c) 2019, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.retrace;
+
+import com.android.tools.r8.DiagnosticsHandler;
+import com.android.tools.r8.naming.ClassNameMapper;
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Deque;
+import java.util.List;
+
+public final class RetraceCore {
+
+  public static class StackTraceNode {
+    private final List<StackTraceNode> children = new ArrayList<>();
+    // TODO(b/132850880): This is not the final design of a node, but we need a placeholder for
+    //  writing tests.
+    public String line;
+
+    @Override
+    public String toString() {
+      return line;
+    }
+  }
+
+  public static class RetraceResult {
+
+    StackTraceNode root;
+
+    RetraceResult(StackTraceNode root) {
+      this.root = root;
+    }
+
+    public List<String> toList() {
+      ArrayList<String> stackTrace = new ArrayList<>();
+      if (root == null) {
+        return stackTrace;
+      }
+      Deque<StackTraceNode> nodes = new ArrayDeque<>();
+      nodes.addLast(root);
+      while (!nodes.isEmpty()) {
+        StackTraceNode currentNode = nodes.removeFirst();
+        stackTrace.add(currentNode.line);
+        for (StackTraceNode child : currentNode.children) {
+          assert child != null;
+          nodes.addLast(child);
+        }
+      }
+      return stackTrace;
+    }
+  }
+
+  private final ClassNameMapper classNameMapper;
+  private final List<String> stackTrace;
+  private final DiagnosticsHandler diagnosticsHandler;
+
+  public RetraceCore(
+      ClassNameMapper classNameMapper,
+      List<String> stackTrace,
+      DiagnosticsHandler diagnosticsHandler) {
+    this.classNameMapper = classNameMapper;
+    this.stackTrace = stackTrace;
+    this.diagnosticsHandler = diagnosticsHandler;
+  }
+
+  public RetraceResult retrace() {
+    return new RetraceResult(retraceLine(stackTrace, 0));
+  }
+
+  private StackTraceNode retraceLine(List<String> stackTrace, int index) {
+    if (stackTrace.size() <= index) {
+      return null;
+    }
+    StackTraceNode node = new StackTraceNode();
+    node.line = stackTrace.get(index);
+    StackTraceNode childNode = retraceLine(stackTrace, index + 1);
+    if (childNode != null) {
+      node.children.add(childNode);
+    }
+    return node;
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/ToolHelper.java b/src/test/java/com/android/tools/r8/ToolHelper.java
index 6d5a0fe..b0f00db 100644
--- a/src/test/java/com/android/tools/r8/ToolHelper.java
+++ b/src/test/java/com/android/tools/r8/ToolHelper.java
@@ -1853,7 +1853,7 @@
     public final String stderr;
     public final String command;
 
-    ProcessResult(int exitCode, String stdout, String stderr, String command) {
+    public ProcessResult(int exitCode, String stdout, String stderr, String command) {
       this.exitCode = exitCode;
       this.stdout = stdout;
       this.stderr = stderr;
diff --git a/src/test/java/com/android/tools/r8/retrace/RetraceCommandLineTests.java b/src/test/java/com/android/tools/r8/retrace/RetraceCommandLineTests.java
new file mode 100644
index 0000000..087e594
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/retrace/RetraceCommandLineTests.java
@@ -0,0 +1,155 @@
+// Copyright (c) 2019, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.retrace;
+
+import static com.android.tools.r8.ToolHelper.LINE_SEPARATOR;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.core.StringContains.containsString;
+import static org.junit.Assert.assertEquals;
+
+import com.android.tools.r8.TestRuntime.CfVm;
+import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.ToolHelper.ProcessResult;
+import com.android.tools.r8.utils.StringUtils;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.PrintStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import org.hamcrest.Matcher;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+public class RetraceCommandLineTests {
+
+  private static final boolean testExternal = true;
+
+  @Rule public TemporaryFolder folder = new TemporaryFolder();
+
+  @Test
+  public void testPrintIdentityStackTraceFile() throws IOException {
+    runTest("", nonMappableStackTrace, false, nonMappableStackTrace);
+  }
+
+  @Test
+  public void testPrintIdentityStackTraceInput() throws IOException {
+    runTest("", nonMappableStackTrace, true, nonMappableStackTrace);
+  }
+
+  @Test
+  public void testNoMappingFileSpecified() throws IOException {
+    runAbortTest(containsString("Mapping file not specified"));
+  }
+
+  @Test
+  public void testMissingMappingFile() throws IOException {
+    runAbortTest(containsString("Could not find mapping file 'foo.txt'"), "foo.txt");
+  }
+
+  @Test
+  public void testVerbose() throws IOException {
+    runAbortTest(containsString("Currently no support for --verbose"), "--verbose");
+  }
+
+  @Test
+  public void testEmpty() throws IOException {
+    runTest("", "", false, "");
+  }
+
+  @Test
+  public void testHelp() throws IOException {
+    ProcessResult processResult = runRetraceCommandLine(null, "--help");
+    assertEquals(0, processResult.exitCode);
+    assertEquals(Retrace.USAGE_MESSAGE, processResult.stdout);
+  }
+
+  private final String nonMappableStackTrace =
+      "com.android.r8.R8Exception: Problem when compiling program\n"
+          + "    at r8.a.a(App:42)\n"
+          + "    at r8.a.b(App:10)\n"
+          + "    at r8.a.c(App:266)\n"
+          + "    at r8.main(App:800)\n"
+          + "Caused by: com.android.r8.R8InnerException: You have to write the program first\n"
+          + "    at r8.retrace(App:184)\n"
+          + "    ... 7 more\n";
+
+  private void runTest(String mapping, String stackTrace, boolean stacktraceStdIn, String expected)
+      throws IOException {
+    ProcessResult result = runRetrace(mapping, stackTrace, stacktraceStdIn);
+    assertEquals(0, result.exitCode);
+    assertEquals(expected, result.stdout);
+  }
+
+  private void runAbortTest(Matcher<String> errorMatch, String... args) throws IOException {
+    ProcessResult result = runRetraceCommandLine(null, args);
+    assertEquals(1, result.exitCode);
+    assertThat(result.stderr, errorMatch);
+  }
+
+  private ProcessResult runRetrace(String mapping, String stackTrace, boolean stacktraceStdIn)
+      throws IOException {
+    Path mappingFile = folder.newFile("mapping.txt").toPath();
+    Files.write(mappingFile, mapping.getBytes());
+    File stackTraceFile = folder.newFile("stacktrace.txt");
+    Files.write(stackTraceFile.toPath(), stackTrace.getBytes());
+    if (stacktraceStdIn) {
+      return runRetraceCommandLine(stackTraceFile, mappingFile.toString());
+    } else {
+      return runRetraceCommandLine(
+          null, mappingFile.toString(), stackTraceFile.toPath().toString());
+    }
+  }
+
+  private ProcessResult runRetraceCommandLine(File stdInput, String... args) throws IOException {
+    if (testExternal) {
+      List<String> command = new ArrayList<>();
+      command.add(ToolHelper.getJavaExecutable(CfVm.JDK8));
+      command.add("-ea");
+      command.add("-cp");
+      command.add(ToolHelper.R8_JAR.toString());
+      command.add("com.android.tools.r8.retrace.Retrace");
+      command.addAll(Arrays.asList(args));
+      ProcessBuilder builder = new ProcessBuilder(command);
+      if (stdInput != null) {
+        builder.redirectInput(stdInput);
+      }
+      return ToolHelper.runProcess(builder);
+    } else {
+      InputStream originalIn = System.in;
+      PrintStream originalOut = System.out;
+      PrintStream originalErr = System.err;
+      if (stdInput != null) {
+        System.setIn(new FileInputStream(stdInput));
+      }
+      ByteArrayOutputStream outputByteStream = new ByteArrayOutputStream();
+      System.setOut(new PrintStream(outputByteStream));
+      ByteArrayOutputStream errorByteStream = new ByteArrayOutputStream();
+      System.setErr(new PrintStream(errorByteStream));
+      int exitCode = 0;
+      try {
+        Retrace.run(args);
+      } catch (Throwable t) {
+        exitCode = 1;
+      }
+      if (originalIn != null) {
+        System.setIn(originalIn);
+      }
+      System.setOut(originalOut);
+      System.setErr(originalErr);
+      return new ProcessResult(
+          exitCode,
+          outputByteStream.toString(),
+          errorByteStream.toString(),
+          StringUtils.join(LINE_SEPARATOR, args));
+    }
+  }
+}