Add support for calculating the feature splits from jars

Bug: 71526824
Change-Id: I2b8cf6af5a8445c888028ecaa25f22d072c5371c
diff --git a/src/main/java/com/android/tools/r8/DexSplitterHelper.java b/src/main/java/com/android/tools/r8/DexSplitterHelper.java
index 0c9fbe8..c086c7e 100644
--- a/src/main/java/com/android/tools/r8/DexSplitterHelper.java
+++ b/src/main/java/com/android/tools/r8/DexSplitterHelper.java
@@ -29,7 +29,10 @@
 public class DexSplitterHelper {
 
   public static void run(
-      D8Command command, String featureSplitMapping, String outputArchive, String proguardMap)
+      D8Command command,
+      FeatureClassMapping featureClassMapping,
+      String outputArchive,
+      String proguardMap)
       throws IOException, CompilationException, ExecutionException {
     InternalOptions options = command.getInternalOptions();
     options.enableDesugaring = false;
@@ -45,8 +48,7 @@
         Timing timing = new Timing("DexSplitter");
         DexApplication app =
             new ApplicationReader(command.getInputApp(), options, timing).read(null, executor);
-        FeatureClassMapping featureClassMapping =
-            new FeatureClassMapping(Paths.get(featureSplitMapping));
+
 
         ClassNameMapper mapper = null;
         if (proguardMap != null) {
diff --git a/src/main/java/com/android/tools/r8/dexsplitter/DexSplitter.java b/src/main/java/com/android/tools/r8/dexsplitter/DexSplitter.java
index 8e184c0..20cf4ee 100644
--- a/src/main/java/com/android/tools/r8/dexsplitter/DexSplitter.java
+++ b/src/main/java/com/android/tools/r8/dexsplitter/DexSplitter.java
@@ -9,6 +9,9 @@
 import com.android.tools.r8.D8Command;
 import com.android.tools.r8.DexIndexedConsumer;
 import com.android.tools.r8.DexSplitterHelper;
+import com.android.tools.r8.ResourceException;
+import com.android.tools.r8.utils.FeatureClassMapping;
+import com.android.tools.r8.utils.FeatureClassMapping.FeatureMappingException;
 import com.android.tools.r8.utils.OptionsParsing;
 import com.android.tools.r8.utils.OptionsParsing.ParseContext;
 import java.io.IOException;
@@ -25,6 +28,7 @@
 
   private static class Options {
     List<String> inputArchives = new ArrayList<>();
+    List<String> featureJars = new ArrayList<>();
     String splitBaseName = DEFAULT_OUTPUT_ARCHIVE_FILENAME;
     String featureSplitMapping;
     String proguardMap;
@@ -39,6 +43,11 @@
         options.inputArchives.addAll(input);
         continue;
       }
+      List<String> featureJars = OptionsParsing.tryParseMulti(context, "--feature-jar");
+      if (featureJars != null) {
+        options.featureJars.addAll(featureJars);
+        continue;
+      }
       String output = OptionsParsing.tryParseSingle(context, "--output", "-o");
       if (output != null) {
         options.splitBaseName = output;
@@ -59,14 +68,27 @@
     return options;
   }
 
+  private static FeatureClassMapping createFeatureClassMapping(Options options)
+      throws IOException, FeatureMappingException, ResourceException {
+    if (options.featureSplitMapping != null) {
+      return FeatureClassMapping.fromSpecification(Paths.get(options.featureSplitMapping));
+    }
+    assert !options.featureJars.isEmpty();
+    return FeatureClassMapping.fromJarFiles(options.featureJars);
+  }
+
   public static void run(String[] args)
-      throws CompilationFailedException, IOException, CompilationException, ExecutionException {
+      throws CompilationFailedException, IOException, CompilationException, ExecutionException,
+          ResourceException, FeatureMappingException {
     Options options = parseArguments(args);
     if (options.inputArchives.isEmpty()) {
       throw new RuntimeException("Need at least one --input");
     }
-    if (options.featureSplitMapping == null) {
-      throw new RuntimeException("You must supply a feature split mapping");
+    if (options.featureSplitMapping == null && options.featureJars.isEmpty()) {
+      throw new RuntimeException("You must supply a feature split mapping or feature jars");
+    }
+    if (options.featureSplitMapping != null && !options.featureJars.isEmpty()) {
+      throw new RuntimeException("You can't supply both a feature split mapping and feature jars");
     }
 
     D8Command.Builder builder = D8Command.builder();
@@ -76,8 +98,11 @@
     // We set the actual consumer on the ApplicationWriter when we have calculated the distribution
     // since we don't yet know the distribution.
     builder.setProgramConsumer(DexIndexedConsumer.emptyConsumer());
+
+    FeatureClassMapping featureClassMapping = createFeatureClassMapping(options);
+
     DexSplitterHelper.run(
-        builder.build(), options.featureSplitMapping, options.splitBaseName, options.proguardMap);
+        builder.build(), featureClassMapping, options.splitBaseName, options.proguardMap);
   }
 
   public static void main(String[] args) {
@@ -89,7 +114,9 @@
     } catch (CompilationFailedException
         | IOException
         | CompilationException
-        | ExecutionException e) {
+        | ExecutionException
+        | ResourceException
+        | FeatureMappingException e) {
       System.err.println("Splitting failed: " + e.getMessage());
       System.exit(1);
     }
diff --git a/src/main/java/com/android/tools/r8/utils/DescriptorUtils.java b/src/main/java/com/android/tools/r8/utils/DescriptorUtils.java
index 1d424bb..cccbb4b 100644
--- a/src/main/java/com/android/tools/r8/utils/DescriptorUtils.java
+++ b/src/main/java/com/android/tools/r8/utils/DescriptorUtils.java
@@ -92,16 +92,29 @@
   }
 
   /**
-   * Determine the given {@param typeName} is valid java type name or not.
+   * Determine the given {@param typeName} is a valid jvms binary name or not (jvms 4.2.1).
    *
-   * @param typeName the java type name
-   * @return true if and only if the given type name is valid java type
+   * @param typeName the jvms binary name
+   * @return true if and only if the given type name is valid jvms binary name
    */
   public static boolean isValidJavaType(String typeName) {
-    return typeName.length() > 0
-        && Character.isJavaIdentifierStart(typeName.charAt(0))
-        && typeName.substring(1).chars().allMatch(
-            ch -> Character.isJavaIdentifierPart(ch) || ch == JAVA_PACKAGE_SEPARATOR);
+    if (typeName.length() == 0) {
+      return false;
+    }
+    char last = 0;
+    for (int i = 0; i < typeName.length(); i++) {
+      char c = typeName.charAt(i);
+      if (c == ';' ||
+          c == '[' ||
+          c == '/') {
+        return false;
+      }
+      if (c == '.' && (i == 0 || last == '.')) {
+        return false;
+      }
+      last = c;
+    }
+    return true;
   }
 
   /**
diff --git a/src/main/java/com/android/tools/r8/utils/FeatureClassMapping.java b/src/main/java/com/android/tools/r8/utils/FeatureClassMapping.java
index a90f193..ebb692f 100644
--- a/src/main/java/com/android/tools/r8/utils/FeatureClassMapping.java
+++ b/src/main/java/com/android/tools/r8/utils/FeatureClassMapping.java
@@ -3,8 +3,10 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.utils;
 
+import com.android.tools.r8.ArchiveClassFileProvider;
 import java.io.IOException;
 import java.nio.file.Path;
+import java.nio.file.Paths;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
@@ -35,9 +37,40 @@
   static final String COMMENT = "#";
   static final String SEPARATOR = ":";
 
-  public FeatureClassMapping(Path file) throws IOException, FeatureMappingException {
-    this(FileUtils.readAllLines(file));
-    this.mappingFile = file;
+  public static FeatureClassMapping fromSpecification(Path file)
+      throws FeatureMappingException, IOException {
+    FeatureClassMapping mapping = new FeatureClassMapping();
+    List<String> lines = FileUtils.readAllLines(file);
+    for (int i = 0; i < lines.size(); i++) {
+      String line = lines.get(i);
+      mapping.parseAndAdd(line, i);
+    }
+    return mapping;
+  }
+
+  public static FeatureClassMapping fromJarFiles(List<String> jarFiles)
+      throws FeatureMappingException, IOException {
+    FeatureClassMapping mapping = new FeatureClassMapping();
+    for (String jar : jarFiles) {
+      Path jarPath = Paths.get(jar);
+      String featureName = jarPath.getFileName().toString();
+      if (featureName.endsWith(".jar") || featureName.endsWith(".zip")) {
+        featureName = featureName.substring(0, featureName.length() - 4);
+      }
+
+      ArchiveClassFileProvider provider = new ArchiveClassFileProvider(jarPath);
+      for (String javaDescriptor : provider.getClassDescriptors()) {
+          String javaType = DescriptorUtils.descriptorToJavaType(javaDescriptor);
+          mapping.addMapping(javaType, featureName);
+      }
+    }
+    return mapping;
+  }
+
+  private FeatureClassMapping() {}
+
+  public void addMapping(String clazz, String feature) throws FeatureMappingException {
+    addRule(clazz, feature, 0);
   }
 
   FeatureClassMapping(List<String> lines) throws FeatureMappingException {
@@ -79,14 +112,21 @@
       error("Mapping lines can only contain one " + SEPARATOR, lineNumber);
     }
 
-    if (parsedRules.containsKey(values[0])) {
-      if (!parsedRules.get(values[0]).equals(values[1])) {
-        error("Redefinition of predicate not allowed", lineNumber);
+    String predicate = values[0];
+    String feature = values[1];
+    addRule(predicate, feature, lineNumber);
+  }
+
+  private void addRule(String predicate, String feature, int lineNumber)
+      throws FeatureMappingException {
+    if (parsedRules.containsKey(predicate)) {
+      if (!parsedRules.get(predicate).equals(feature)) {
+        error("Redefinition of predicate " + predicate + "not allowed", lineNumber);
       }
       return; // Already have this rule.
     }
-    parsedRules.put(values[0], values[1]);
-    FeaturePredicate featurePredicate = new FeaturePredicate(values[0], values[1]);
+    parsedRules.put(predicate, feature);
+    FeaturePredicate featurePredicate = new FeaturePredicate(predicate, feature);
     mappings.add(featurePredicate);
   }
 
@@ -103,7 +143,7 @@
 
   /** A feature predicate can either be a wildcard or class predicate. */
   private static class FeaturePredicate {
-
+    private static Pattern identifier = Pattern.compile("[A-Za-z_\\-][A-Za-z0-9_$\\-]*");
     final String predicate;
     final String feature;
     // False implies class predicate.
@@ -116,18 +156,10 @@
       } else {
         this.predicate = predicate;
       }
-      validateIdentifiers(this.predicate.split("\\."));
-      this.feature = feature;
-    }
-
-    private void validateIdentifiers(String[] names) throws FeatureMappingException {
-      // TODO(ricow): knap saa mange regexp.
-      Pattern p = Pattern.compile("[A-Za-z_][A-Za-z0-9_$]*");
-      for (String name : names) {
-        if (!p.matcher(name).matches()) {
-          throw new FeatureMappingException(name + " is not a valid identifier");
-        }
+      if (!DescriptorUtils.isValidJavaType(this.predicate)) {
+        throw new FeatureMappingException(this.predicate + " is not a valid identifier");
       }
+      this.feature = feature;
     }
 
     boolean match(String className) {
diff --git a/src/test/java/com/android/tools/r8/dexsplitter/DexSplitterTests.java b/src/test/java/com/android/tools/r8/dexsplitter/DexSplitterTests.java
index 2e8938a..750a5c8 100644
--- a/src/test/java/com/android/tools/r8/dexsplitter/DexSplitterTests.java
+++ b/src/test/java/com/android/tools/r8/dexsplitter/DexSplitterTests.java
@@ -23,10 +23,13 @@
 import java.io.IOException;
 import java.io.PrintWriter;
 import java.io.UnsupportedEncodingException;
+import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.Paths;
 import java.util.List;
 import java.util.concurrent.ExecutionException;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipOutputStream;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.rules.TemporaryFolder;
@@ -71,9 +74,14 @@
           "--feature-splits", splitSpec.toString()
         });
 
-    // Both classes should still work if we give all dex files to the system.
+
     Path base = outputDex.getParent().resolve("output.base.zip");
     Path feature = outputDex.getParent().resolve("output.feature1.zip");
+    validateUnobfuscatedOutput(base, feature);
+  }
+
+  private void validateUnobfuscatedOutput(Path base, Path feature) throws IOException {
+    // Both classes should still work if we give all dex files to the system.
     for (String className : new String[] {"Class1", "Class2", "Class3"}) {
       ArtCommandBuilder builder = new ArtCommandBuilder();
       builder.appendClasspath(base.toString());
@@ -128,6 +136,60 @@
   }
 
   @Test
+  public void splitFilesFromJar() throws IOException, CompilationFailedException {
+    // Initial normal compile to create dex files.
+    Path inputDex = temp.newFolder().toPath().resolve("input.zip");
+    D8.run(
+        D8Command.builder()
+            .setOutput(inputDex, OutputMode.DexIndexed)
+            .addProgramFiles(Paths.get(CLASS1_CLASS))
+            .addProgramFiles(Paths.get(CLASS2_CLASS))
+            .addProgramFiles(Paths.get(CLASS3_CLASS))
+            .addProgramFiles(Paths.get(CLASS3_INNER_CLASS))
+            .build());
+
+    Path outputDex = temp.getRoot().toPath().resolve("output");
+    Path baseJar = temp.getRoot().toPath().resolve("base.jar");
+    Path featureJar = temp.getRoot().toPath().resolve("feature1.jar");
+    ZipOutputStream baseStream = new ZipOutputStream(Files.newOutputStream(baseJar));
+    String name = "dexsplitsample/Class1.class";
+    baseStream.putNextEntry(new ZipEntry(name));
+    baseStream.write(Files.readAllBytes(Paths.get(CLASS1_CLASS)));
+    baseStream.closeEntry();
+    baseStream.close();
+
+    ZipOutputStream featureStream = new ZipOutputStream(Files.newOutputStream(featureJar));
+    name = "dexsplitsample/Class2.class";
+    featureStream.putNextEntry(new ZipEntry(name));
+    featureStream.write(Files.readAllBytes(Paths.get(CLASS2_CLASS)));
+    featureStream.closeEntry();
+    name = "dexsplitsample/Class3.class";
+    featureStream.putNextEntry(new ZipEntry(name));
+    featureStream.write(Files.readAllBytes(Paths.get(CLASS3_CLASS)));
+    featureStream.closeEntry();
+    name = "dexsplitsample/Class3$InnerClass.class";
+    featureStream.putNextEntry(new ZipEntry(name));
+    featureStream.write(Files.readAllBytes(Paths.get(CLASS3_INNER_CLASS)));
+    featureStream.closeEntry();
+    featureStream.close();
+
+    DexSplitter.main(
+        new String[] {
+          "--input",
+          inputDex.toString(),
+          "--output",
+          outputDex.toString(),
+          "--feature-jar",
+          baseJar.toString(),
+          "--feature-jar",
+          featureJar.toString()
+        });
+    Path base = outputDex.getParent().resolve("output.base.zip");
+    Path feature = outputDex.getParent().resolve("output.feature1.zip");
+    validateUnobfuscatedOutput(base, feature);
+  }
+
+  @Test
   public void splitFilesObfuscation()
       throws CompilationFailedException, IOException, ExecutionException {
     // Initial normal compile to create dex files.
diff --git a/src/test/java/com/android/tools/r8/utils/FeatureClassMappingTest.java b/src/test/java/com/android/tools/r8/utils/FeatureClassMappingTest.java
index ba571fb..3e759d9 100644
--- a/src/test/java/com/android/tools/r8/utils/FeatureClassMappingTest.java
+++ b/src/test/java/com/android/tools/r8/utils/FeatureClassMappingTest.java
@@ -70,9 +70,6 @@
     ensureThrowsMappingException("com.google.base");
     // Two colons.
     ensureThrowsMappingException(ImmutableList.of("a:b:c"));
-    // Invalid identifiers.
-    ensureThrowsMappingException("12com.google.base:feature1");
-    ensureThrowsMappingException("$com.google.base:feature1");
 
     // Empty identifier.
     ensureThrowsMappingException("com..google:feature1");