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));
+ }
+ }
+}