Merge "Support embedded Proguard rules in META-INF/proguard/*"
diff --git a/src/main/java/com/android/tools/r8/R8Command.java b/src/main/java/com/android/tools/r8/R8Command.java
index a52d694..8a46d57 100644
--- a/src/main/java/com/android/tools/r8/R8Command.java
+++ b/src/main/java/com/android/tools/r8/R8Command.java
@@ -12,9 +12,11 @@
 import com.android.tools.r8.shaking.ProguardConfigurationParser;
 import com.android.tools.r8.shaking.ProguardConfigurationRule;
 import com.android.tools.r8.shaking.ProguardConfigurationSource;
+import com.android.tools.r8.shaking.ProguardConfigurationSourceBytes;
 import com.android.tools.r8.shaking.ProguardConfigurationSourceFile;
 import com.android.tools.r8.shaking.ProguardConfigurationSourceStrings;
 import com.android.tools.r8.utils.AndroidApp;
+import com.android.tools.r8.utils.ExceptionDiagnostic;
 import com.android.tools.r8.utils.FileUtils;
 import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.InternalOptions.LineNumberOptimization;
@@ -22,15 +24,17 @@
 import com.android.tools.r8.utils.StringDiagnostic;
 import com.google.common.collect.ImmutableList;
 import java.io.IOException;
+import java.io.InputStream;
 import java.nio.file.Path;
 import java.nio.file.Paths;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.List;
+import java.util.Objects;
 import java.util.function.Consumer;
 
 /**
- * Immutable command structure for an invocation of the {@link D8} compiler.
+ * Immutable command structure for an invocation of the {@link R8} compiler.
  *
  * <p>To build a R8 command use the {@link R8Command.Builder} class. For example:
  *
@@ -282,21 +286,54 @@
         mainDexKeepRules = parser.getConfig().getRules();
       }
 
-      ProguardConfiguration.Builder configurationBuilder;
-      if (proguardConfigs.isEmpty()) {
-        configurationBuilder = ProguardConfiguration.builder(factory, reporter);
-      } else {
-        ProguardConfigurationParser parser =
-            new ProguardConfigurationParser(factory, reporter);
+      ProguardConfigurationParser parser = new ProguardConfigurationParser(factory, reporter);
+      if (!proguardConfigs.isEmpty()) {
         parser.parse(proguardConfigs);
-        configurationBuilder = parser.getConfigurationBuilder();
-        configurationBuilder.setForceProguardCompatibility(forceProguardCompatibility);
       }
+      ProguardConfiguration.Builder configurationBuilder = parser.getConfigurationBuilder();
+      configurationBuilder.setForceProguardCompatibility(forceProguardCompatibility);
 
       if (proguardConfigurationConsumer != null) {
         proguardConfigurationConsumer.accept(configurationBuilder);
       }
 
+      // Process Proguard configurations supplied through data resources in the input.
+      DataResourceProvider.Visitor embeddedProguardConfigurationVisitor =
+          new DataResourceProvider.Visitor() {
+            @Override
+            public void visit(DataDirectoryResource directory) {
+              // Don't do anything.
+            }
+
+            @Override
+            public void visit(DataEntryResource resource) {
+              if (resource.getName().startsWith("META-INF/proguard/")) {
+                try (InputStream in = resource.getByteStream()) {
+                  ProguardConfigurationSource source =
+                      new ProguardConfigurationSourceBytes(in, resource.getOrigin());
+                  parser.parse(source);
+                } catch (ResourceException e) {
+                  reporter.error(new StringDiagnostic("Failed to open input: " + e.getMessage(),
+                      resource.getOrigin()));
+                } catch (Exception e) {
+                  reporter.error(new ExceptionDiagnostic(e, resource.getOrigin()));
+                }
+              }
+            }
+          };
+
+      getAppBuilder().getProgramResourceProviders()
+          .stream()
+          .map(ProgramResourceProvider::getDataResourceProvider)
+          .filter(Objects::nonNull)
+          .forEach(dataResourceProvider -> {
+              try {
+                dataResourceProvider.accept(embeddedProguardConfigurationVisitor);
+              } catch (ResourceException e) {
+                reporter.error(new ExceptionDiagnostic(e));
+              }
+          });
+
       if (disableTreeShaking) {
         configurationBuilder.disableShrinking();
       }
@@ -369,6 +406,11 @@
       }
       return resources;
     }
+
+    @Override
+    public DataResourceProvider getDataResourceProvider() {
+      return provider.getDataResourceProvider();
+    }
   }
 
   // Internal state to verify parsing properties not enforced by the builder.
diff --git a/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationParser.java b/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationParser.java
index 5f5237b..91cab92 100644
--- a/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationParser.java
+++ b/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationParser.java
@@ -137,8 +137,7 @@
     parse(ImmutableList.of(new ProguardConfigurationSourceFile(path)));
   }
 
-  // package visible for testing
-  void parse(ProguardConfigurationSource source) {
+  public void parse(ProguardConfigurationSource source) {
     parse(ImmutableList.of(source));
   }
 
@@ -968,6 +967,11 @@
     private Path parseFileName() throws ProguardRuleParserException {
       TextPosition start = getPosition();
       skipWhitespace();
+
+      if (baseDirectory == null) {
+        throw parseError("Options with file names are not supported", start);
+      }
+
       String fileName = acceptString(character ->
           character != File.pathSeparatorChar
               && !Character.isWhitespace(character)
diff --git a/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationSourceBytes.java b/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationSourceBytes.java
new file mode 100644
index 0000000..3f379a4
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationSourceBytes.java
@@ -0,0 +1,47 @@
+// Copyright (c) 2018, 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.shaking;
+
+import com.android.tools.r8.origin.Origin;
+import com.google.common.io.ByteStreams;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Path;
+
+public class ProguardConfigurationSourceBytes implements ProguardConfigurationSource {
+  private final byte[] bytes;
+  private final Origin origin;
+
+  public ProguardConfigurationSourceBytes(byte[] bytes, Origin origin) {
+    this.bytes = bytes;
+    this.origin = origin;
+  }
+
+  public ProguardConfigurationSourceBytes(InputStream in, Origin origin) throws IOException {
+    this(ByteStreams.toByteArray(in), origin);
+  }
+
+  @Override
+  public String get() throws IOException {
+    return new String(bytes, StandardCharsets.UTF_8);
+  }
+
+  @Override
+  public Path getBaseDirectory() {
+    // Options with relative names (including -include) is not supported.
+    return null;
+  }
+
+  @Override
+  public String getName() {
+    return origin.toString();
+  }
+
+  @Override
+  public Origin getOrigin() {
+    return origin;
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/utils/AndroidApp.java b/src/main/java/com/android/tools/r8/utils/AndroidApp.java
index d8dffea..1b026c0 100644
--- a/src/main/java/com/android/tools/r8/utils/AndroidApp.java
+++ b/src/main/java/com/android/tools/r8/utils/AndroidApp.java
@@ -589,5 +589,9 @@
         throw new CompilationError("Unsupported source file type", new PathOrigin(file));
       }
     }
+
+    public List<ProgramResourceProvider> getProgramResourceProviders() {
+      return programResourceProviders;
+    }
   }
 }
diff --git a/src/main/java/com/android/tools/r8/utils/ExceptionDiagnostic.java b/src/main/java/com/android/tools/r8/utils/ExceptionDiagnostic.java
index 9fd91d9..99149ff 100644
--- a/src/main/java/com/android/tools/r8/utils/ExceptionDiagnostic.java
+++ b/src/main/java/com/android/tools/r8/utils/ExceptionDiagnostic.java
@@ -4,6 +4,7 @@
 
 package com.android.tools.r8.utils;
 
+import com.android.tools.r8.ResourceException;
 import com.android.tools.r8.origin.Origin;
 import com.android.tools.r8.position.Position;
 import java.io.FileNotFoundException;
@@ -19,6 +20,10 @@
     this.origin = origin;
   }
 
+  public ExceptionDiagnostic(ResourceException e) {
+    this(e, e.getOrigin());
+  }
+
   @Override
   public Origin getOrigin() {
     return origin;
diff --git a/src/test/java/com/android/tools/r8/DiagnosticsChecker.java b/src/test/java/com/android/tools/r8/DiagnosticsChecker.java
index fc22895..2265c3e 100644
--- a/src/test/java/com/android/tools/r8/DiagnosticsChecker.java
+++ b/src/test/java/com/android/tools/r8/DiagnosticsChecker.java
@@ -3,14 +3,23 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8;
 
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertSame;
 import static org.junit.Assert.assertTrue;
 
+import com.android.tools.r8.graph.invokesuper.Consumer;
+import com.android.tools.r8.origin.Origin;
+import com.android.tools.r8.origin.PathOrigin;
+import com.android.tools.r8.position.Position;
+import com.android.tools.r8.position.TextPosition;
+import com.android.tools.r8.position.TextRange;
 import com.android.tools.r8.utils.ListUtils;
+import java.nio.file.Path;
 import java.util.ArrayList;
 import java.util.List;
 
 // Helper to check that a particular error occurred.
-class DiagnosticsChecker implements DiagnosticsHandler {
+public class DiagnosticsChecker implements DiagnosticsHandler {
   public List<Diagnostic> errors = new ArrayList<>();
   public List<Diagnostic> warnings = new ArrayList<>();
   public List<Diagnostic> infos = new ArrayList<>();
@@ -51,4 +60,64 @@
       throw e;
     }
   }
+
+  public static Diagnostic checkDiagnostic(Diagnostic diagnostic, Consumer<Origin> originChecker,
+      int lineStart, int columnStart, String... messageParts) {
+    if (originChecker != null) {
+      originChecker.accept(diagnostic.getOrigin());
+    }
+    TextPosition position;
+    if (diagnostic.getPosition() instanceof TextRange) {
+      position = ((TextRange) diagnostic.getPosition()).getStart();
+    } else {
+      position = ((TextPosition) diagnostic.getPosition());
+    }
+    assertEquals(lineStart, position.getLine());
+    assertEquals(columnStart, position.getColumn());
+    for (String part : messageParts) {
+      assertTrue(diagnostic.getDiagnosticMessage() + " doesn't contain \"" + part + "\"",
+          diagnostic.getDiagnosticMessage().contains(part));
+    }
+    return diagnostic;
+  }
+
+  public static Diagnostic checkDiagnostic(Diagnostic diagnostic, Consumer<Origin> originChecker,
+      String... messageParts) {
+    if (originChecker != null) {
+      originChecker.accept(diagnostic.getOrigin());
+    }
+    assertEquals(diagnostic.getPosition(), Position.UNKNOWN);
+    for (String part : messageParts) {
+      assertTrue(diagnostic.getDiagnosticMessage() + " doesn't contain \"" + part + "\"",
+          diagnostic.getDiagnosticMessage().contains(part));
+    }
+    return diagnostic;
+  }
+
+  static class PathOriginChecker implements Consumer<Origin> {
+    private final Path path;
+    PathOriginChecker(Path path) {
+      this.path = path;
+    }
+
+    public void accept(Origin origin) {
+      if (path != null) {
+        assertEquals(path, ((PathOrigin) origin).getPath());
+      } else {
+        assertSame(Origin.unknown(), origin);
+      }
+    }
+  }
+
+  public static Diagnostic checkDiagnostic(Diagnostic diagnostic, Path path,
+      int lineStart, int columnStart, String... messageParts) {
+    return checkDiagnostic(diagnostic, new PathOriginChecker(path), lineStart, columnStart,
+        messageParts);
+  }
+
+  public static Diagnostic checkDiagnostics(List<Diagnostic> diagnostics, Path path,
+      int lineStart, int columnStart, String... messageParts) {
+    assertEquals(1, diagnostics.size());
+    return checkDiagnostic(diagnostics.get(0), path, lineStart, columnStart, messageParts);
+  }
 }
diff --git a/src/test/java/com/android/tools/r8/TestBase.java b/src/test/java/com/android/tools/r8/TestBase.java
index 2bb8d75..96591d8 100644
--- a/src/test/java/com/android/tools/r8/TestBase.java
+++ b/src/test/java/com/android/tools/r8/TestBase.java
@@ -153,19 +153,26 @@
   protected Path jarTestClasses(Class... classes) throws IOException {
     Path jar = File.createTempFile("junit", ".jar", temp.getRoot()).toPath();
     try (JarOutputStream out = new JarOutputStream(new FileOutputStream(jar.toFile()))) {
-      for (Class clazz : classes) {
-        try (FileInputStream in =
-            new FileInputStream(ToolHelper.getClassFileForTestClass(clazz).toFile())) {
-          out.putNextEntry(new ZipEntry(ToolHelper.getJarEntryForTestClass(clazz)));
-          ByteStreams.copy(in, out);
-          out.closeEntry();
-        }
-      }
+      addTestClassesToJar(out, classes);
     }
     return jar;
   }
 
   /**
+   * Create a temporary JAR file containing the specified test classes.
+   */
+  protected void addTestClassesToJar(JarOutputStream out, Class... classes) throws IOException {
+    for (Class clazz : classes) {
+      try (FileInputStream in =
+          new FileInputStream(ToolHelper.getClassFileForTestClass(clazz).toFile())) {
+        out.putNextEntry(new ZipEntry(ToolHelper.getJarEntryForTestClass(clazz)));
+        ByteStreams.copy(in, out);
+        out.closeEntry();
+      }
+    }
+  }
+
+  /**
    * Create a temporary JAR file containing all test classes in a package.
    */
   protected Path jarTestClassesInPackage(Package pkg) throws IOException {
diff --git a/src/test/java/com/android/tools/r8/shaking/LibraryProvidedProguardRulesTest.java b/src/test/java/com/android/tools/r8/shaking/LibraryProvidedProguardRulesTest.java
new file mode 100644
index 0000000..6ecdb09
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/shaking/LibraryProvidedProguardRulesTest.java
@@ -0,0 +1,201 @@
+// Copyright (c) 2018, 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.shaking;
+
+import static com.android.tools.r8.utils.DexInspectorMatchers.isPresent;
+import static org.hamcrest.CoreMatchers.not;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import com.android.tools.r8.CompilationFailedException;
+import com.android.tools.r8.DataResourceProvider;
+import com.android.tools.r8.DexIndexedConsumer;
+import com.android.tools.r8.DiagnosticsChecker;
+import com.android.tools.r8.DiagnosticsHandler;
+import com.android.tools.r8.ProgramResource;
+import com.android.tools.r8.ProgramResource.Kind;
+import com.android.tools.r8.ProgramResourceProvider;
+import com.android.tools.r8.R8Command;
+import com.android.tools.r8.ResourceException;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.origin.ArchiveEntryOrigin;
+import com.android.tools.r8.origin.Origin;
+import com.android.tools.r8.origin.PathOrigin;
+import com.android.tools.r8.utils.AndroidApp;
+import com.android.tools.r8.utils.DescriptorUtils;
+import com.android.tools.r8.utils.DexInspector;
+import com.google.common.collect.ImmutableList;
+import com.google.common.io.ByteStreams;
+import com.google.common.io.CharSource;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Path;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.jar.JarOutputStream;
+import java.util.zip.ZipEntry;
+import org.junit.Test;
+
+class A {
+  private static String buildClassName(String className) {
+    return A.class.getPackage().getName() + "." + className;
+  }
+
+  public static void main(String[] args) {
+    try {
+      Class bClass = Class.forName(buildClassName("B"));
+      System.out.println("YES");
+    } catch (ClassNotFoundException e) {
+      System.out.println("NO");
+    }
+  }
+}
+
+class B {
+
+}
+
+public class LibraryProvidedProguardRulesTest extends TestBase {
+  private void addTextJarEntry(JarOutputStream out, String name, String content) throws Exception {
+    out.putNextEntry(new ZipEntry(name));
+    ByteStreams.copy(
+        CharSource.wrap(content).asByteSource(StandardCharsets.UTF_8).openBufferedStream(), out);
+    out.closeEntry();
+  }
+
+  private AndroidApp runTest(List<String> rules, DiagnosticsHandler handler) throws Exception {
+    Path jar = temp.newFile("test.jar").toPath();
+    try (JarOutputStream out = new JarOutputStream(new FileOutputStream(jar.toFile()))) {
+      addTestClassesToJar(out, A.class, B.class);
+      for (int i =  0; i < rules.size(); i++) {
+        String name = "META-INF/proguard/jar" + (i == 0 ? "" : i) + ".rules";
+        addTextJarEntry(out, name, rules.get(i));
+      }
+    }
+
+    try {
+      R8Command command = (handler != null ? R8Command.builder(handler) : R8Command.builder())
+          .addProgramFiles(jar)
+          .setProgramConsumer(DexIndexedConsumer.emptyConsumer())
+          .build();
+      return ToolHelper.runR8(command);
+    } catch (CompilationFailedException e) {
+      assertNotNull(handler);
+      return null;
+    }
+  }
+
+  private AndroidApp runTest(String rules, DiagnosticsHandler handler) throws Exception {
+    return runTest(ImmutableList.of(rules), handler);
+  }
+
+
+  @Test
+  public void keepOnlyA() throws Exception {
+    AndroidApp app = runTest("-keep class " + A.class.getTypeName() +" {}", null);
+    DexInspector inspector = new DexInspector(app);
+    assertThat(inspector.clazz(A.class), isPresent());
+    assertThat(inspector.clazz(B.class), not(isPresent()));
+  }
+
+  @Test
+  public void keepOnlyB() throws Exception {
+    AndroidApp app = runTest("-keep class **B {}", null);
+    DexInspector inspector = new DexInspector(app);
+    assertThat(inspector.clazz(A.class), not(isPresent()));
+    assertThat(inspector.clazz(B.class), isPresent());
+  }
+
+  @Test
+  public void keepBoth() throws Exception {
+    AndroidApp app = runTest("-keep class ** {}", null);
+    DexInspector inspector = new DexInspector(app);
+    assertThat(inspector.clazz(A.class), isPresent());
+    assertThat(inspector.clazz(B.class), isPresent());
+  }
+
+  @Test
+  public void multipleFiles() throws Exception {
+    AndroidApp app = runTest(ImmutableList.of("-keep class **A {}", "-keep class **B {}"), null);
+    DexInspector inspector = new DexInspector(app);
+    assertThat(inspector.clazz(A.class), isPresent());
+    assertThat(inspector.clazz(B.class), isPresent());
+  }
+
+  private void checkOrigin(Origin origin) {
+    assertTrue(origin instanceof ArchiveEntryOrigin);
+    assertEquals(origin.part(), "META-INF/proguard/jar.rules");
+    assertTrue(origin.parent() instanceof PathOrigin);
+  }
+
+  @Test
+  public void syntaxError() throws Exception {
+    DiagnosticsChecker checker = new DiagnosticsChecker();
+    AndroidApp app = runTest("error", checker);
+    assertNull(app);
+    DiagnosticsChecker.checkDiagnostic(
+        checker.errors.get(0), this::checkOrigin, 1, 1, "Expected char '-'");
+  }
+
+  @Test
+  public void includeError() throws Exception {
+    DiagnosticsChecker checker = new DiagnosticsChecker();
+    AndroidApp app = runTest("-include other.rules", checker);
+    assertNull(app);
+    DiagnosticsChecker.checkDiagnostic(checker.errors.get(0), this::checkOrigin, 1, 10,
+        "Options with file names are not supported");
+  }
+
+  class TestProvider implements ProgramResourceProvider, DataResourceProvider {
+
+    @Override
+    public Collection<ProgramResource> getProgramResources() throws ResourceException {
+      byte[] bytes;
+      try {
+        bytes = ByteStreams.toByteArray(A.class.getResourceAsStream("A.class"));
+      } catch (IOException e) {
+        throw new ResourceException(Origin.unknown(), "Unexpected");
+      }
+      return ImmutableList.of(
+          ProgramResource.fromBytes(Origin.unknown(), Kind.CF, bytes,
+              Collections.singleton(DescriptorUtils.javaTypeToDescriptor(A.class.getTypeName()))));
+    }
+
+    @Override
+    public DataResourceProvider getDataResourceProvider() {
+      return this;
+    }
+
+    @Override
+    public void accept(Visitor visitor) throws ResourceException {
+      throw new ResourceException(Origin.unknown(), "Cannot provide data resources after all");
+    }
+  }
+
+  @Test
+  public void throwingDataResourceProvider() throws Exception {
+    DiagnosticsChecker checker = new DiagnosticsChecker();
+    try {
+      R8Command command = R8Command.builder(checker)
+          .addProgramResourceProvider(new TestProvider())
+          .setProgramConsumer(DexIndexedConsumer.emptyConsumer())
+          .build();
+      fail("Should not succeed");
+    } catch (CompilationFailedException e) {
+      DiagnosticsChecker.checkDiagnostic(
+          checker.errors.get(0),
+          origin -> assertSame(origin, Origin.unknown()),
+          "Cannot provide data resources after all");
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/shaking/ProguardConfigurationParserTest.java b/src/test/java/com/android/tools/r8/shaking/ProguardConfigurationParserTest.java
index b7277cb..1012421 100644
--- a/src/test/java/com/android/tools/r8/shaking/ProguardConfigurationParserTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/ProguardConfigurationParserTest.java
@@ -1718,6 +1718,7 @@
     assertEquals(0, handler.errors.size());
   }
 
+  // TODO(sgjesse): Change to use DiagnosticsChecker.
   private Diagnostic checkDiagnostic(List<Diagnostic> diagnostics, Path path, int lineStart,
       int columnStart, String... messageParts) {
     assertEquals(1, diagnostics.size());