summaryrefslogtreecommitdiff
path: root/java/com/google/devtools/build/android/desugar/io
diff options
context:
space:
mode:
Diffstat (limited to 'java/com/google/devtools/build/android/desugar/io')
-rw-r--r--java/com/google/devtools/build/android/desugar/io/BitFlags.java51
-rw-r--r--java/com/google/devtools/build/android/desugar/io/CoreLibraryRewriter.java201
-rw-r--r--java/com/google/devtools/build/android/desugar/io/DirectoryInputFileProvider.java81
-rw-r--r--java/com/google/devtools/build/android/desugar/io/DirectoryOutputFileProvider.java59
-rw-r--r--java/com/google/devtools/build/android/desugar/io/FieldInfo.java29
-rw-r--r--java/com/google/devtools/build/android/desugar/io/HeaderClassLoader.java234
-rw-r--r--java/com/google/devtools/build/android/desugar/io/IndexedInputs.java94
-rw-r--r--java/com/google/devtools/build/android/desugar/io/InputFileProvider.java49
-rw-r--r--java/com/google/devtools/build/android/desugar/io/OutputFileProvider.java45
-rw-r--r--java/com/google/devtools/build/android/desugar/io/ThrowingClassLoader.java27
-rw-r--r--java/com/google/devtools/build/android/desugar/io/ZipInputFileProvider.java64
-rw-r--r--java/com/google/devtools/build/android/desugar/io/ZipOutputFileProvider.java78
12 files changed, 1012 insertions, 0 deletions
diff --git a/java/com/google/devtools/build/android/desugar/io/BitFlags.java b/java/com/google/devtools/build/android/desugar/io/BitFlags.java
new file mode 100644
index 0000000..af6f481
--- /dev/null
+++ b/java/com/google/devtools/build/android/desugar/io/BitFlags.java
@@ -0,0 +1,51 @@
+// Copyright 2016 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.devtools.build.android.desugar.io;
+
+import org.objectweb.asm.Opcodes;
+
+/** Convenience method for working with {@code int} bitwise flags. */
+public class BitFlags {
+
+ /**
+ * Returns {@code true} iff <b>all</b> bits in {@code bitmask} are set in {@code flags}. Trivially
+ * returns {@code true} if {@code bitmask} is 0.
+ */
+ public static boolean isSet(int flags, int bitmask) {
+ return (flags & bitmask) == bitmask;
+ }
+
+ /**
+ * Returns {@code true} iff <b>none</b> of the bits in {@code bitmask} are set in {@code flags}.
+ * Trivially returns {@code true} if {@code bitmask} is 0.
+ */
+ public static boolean noneSet(int flags, int bitmask) {
+ return (flags & bitmask) == 0;
+ }
+
+ public static boolean isInterface(int access) {
+ return isSet(access, Opcodes.ACC_INTERFACE);
+ }
+
+ public static boolean isStatic(int access) {
+ return isSet(access, Opcodes.ACC_STATIC);
+ }
+
+ public static boolean isSynthetic(int access) {
+ return isSet(access, Opcodes.ACC_SYNTHETIC);
+ }
+
+ // Static methods only
+ private BitFlags() {}
+}
diff --git a/java/com/google/devtools/build/android/desugar/io/CoreLibraryRewriter.java b/java/com/google/devtools/build/android/desugar/io/CoreLibraryRewriter.java
new file mode 100644
index 0000000..f3c546c
--- /dev/null
+++ b/java/com/google/devtools/build/android/desugar/io/CoreLibraryRewriter.java
@@ -0,0 +1,201 @@
+// Copyright 2017 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.devtools.build.android.desugar.io;
+
+import java.io.IOException;
+import java.io.InputStream;
+import javax.annotation.Nullable;
+import org.objectweb.asm.Attribute;
+import org.objectweb.asm.ClassReader;
+import org.objectweb.asm.ClassVisitor;
+import org.objectweb.asm.ClassWriter;
+import org.objectweb.asm.Opcodes;
+import org.objectweb.asm.commons.ClassRemapper;
+import org.objectweb.asm.commons.Remapper;
+
+/** Utility class to prefix or unprefix class names of core library classes */
+public class CoreLibraryRewriter {
+ private final String prefix;
+
+ public CoreLibraryRewriter(String prefix) {
+ this.prefix = prefix;
+ }
+
+ /**
+ * Factory method that returns either a normal ClassReader if prefix is empty, or a ClassReader
+ * with a ClassRemapper that prefixes class names of core library classes if prefix is not empty.
+ */
+ public ClassReader reader(InputStream content) throws IOException {
+ if (prefix.isEmpty()) {
+ return new ClassReader(content);
+ } else {
+ return new PrefixingClassReader(content, prefix);
+ }
+ }
+
+ /**
+ * Factory method that returns a ClassVisitor that delegates to a ClassWriter, removing prefix
+ * from core library class names if it is not empty.
+ */
+ public UnprefixingClassWriter writer(int flags) {
+ return new UnprefixingClassWriter(flags);
+ }
+
+ static boolean shouldPrefix(String typeName) {
+ return (typeName.startsWith("java/") || typeName.startsWith("sun/")) && !except(typeName);
+ }
+
+ private static boolean except(String typeName) {
+ if (typeName.startsWith("java/lang/invoke/")) {
+ return true;
+ }
+
+ switch (typeName) {
+ // Autoboxed types
+ case "java/lang/Boolean":
+ case "java/lang/Byte":
+ case "java/lang/Character":
+ case "java/lang/Double":
+ case "java/lang/Float":
+ case "java/lang/Integer":
+ case "java/lang/Long":
+ case "java/lang/Number":
+ case "java/lang/Short":
+
+ // Special types
+ case "java/lang/Class":
+ case "java/lang/Object":
+ case "java/lang/String":
+ case "java/lang/Throwable":
+ return true;
+
+ default: // fall out
+ }
+
+ return false;
+ }
+
+ public String getPrefix() {
+ return prefix;
+ }
+
+ /** Removes prefix from class names */
+ public String unprefix(String typeName) {
+ if (prefix.isEmpty() || !typeName.startsWith(prefix)) {
+ return typeName;
+ }
+ return typeName.substring(prefix.length());
+ }
+
+ /** ClassReader that prefixes core library class names as they are read */
+ private static class PrefixingClassReader extends ClassReader {
+ private final String prefix;
+
+ PrefixingClassReader(InputStream content, String prefix) throws IOException {
+ super(content);
+ this.prefix = prefix;
+ }
+
+ @Override
+ public void accept(ClassVisitor cv, Attribute[] attrs, int flags) {
+ cv =
+ new ClassRemapper(
+ cv,
+ new Remapper() {
+ @Override
+ public String map(String typeName) {
+ return prefix(typeName);
+ }
+ });
+ super.accept(cv, attrs, flags);
+ }
+
+ @Override
+ public String getClassName() {
+ return prefix(super.getClassName());
+ }
+
+ @Override
+ public String getSuperName() {
+ String result = super.getSuperName();
+ return result != null ? prefix(result) : null;
+ }
+
+ @Override
+ public String[] getInterfaces() {
+ String[] result = super.getInterfaces();
+ for (int i = 0, len = result.length; i < len; ++i) {
+ result[i] = prefix(result[i]);
+ }
+ return result;
+ }
+
+ /** Prefixes core library class names with prefix. */
+ private String prefix(String typeName) {
+ if (shouldPrefix(typeName)) {
+ return prefix + typeName;
+ }
+ return typeName;
+ }
+ }
+
+ /**
+ * ClassVisitor that delegates to a ClassWriter, but removes a prefix as each class is written.
+ * The unprefixing is optimized out if prefix is empty.
+ */
+ public class UnprefixingClassWriter extends ClassVisitor {
+ private final ClassWriter writer;
+
+ private String finalClassName;
+
+ UnprefixingClassWriter(int flags) {
+ super(Opcodes.ASM6);
+ this.writer = new ClassWriter(flags);
+ this.cv = this.writer;
+ if (!prefix.isEmpty()) {
+ this.cv =
+ new ClassRemapper(
+ this.writer,
+ new Remapper() {
+ @Override
+ public String map(String typeName) {
+ return unprefix(typeName);
+ }
+ });
+ }
+ }
+
+ /** Returns the (unprefixed) name of the class once written. */
+ @Nullable
+ public String getClassName() {
+ return finalClassName;
+ }
+
+ public byte[] toByteArray() {
+ return writer.toByteArray();
+ }
+
+ @Override
+ public void visit(
+ int version,
+ int access,
+ String name,
+ String signature,
+ String superName,
+ String[] interfaces) {
+ finalClassName = unprefix(name);
+ super.visit(version, access, name, signature, superName, interfaces);
+ }
+ }
+}
diff --git a/java/com/google/devtools/build/android/desugar/io/DirectoryInputFileProvider.java b/java/com/google/devtools/build/android/desugar/io/DirectoryInputFileProvider.java
new file mode 100644
index 0000000..c607b42
--- /dev/null
+++ b/java/com/google/devtools/build/android/desugar/io/DirectoryInputFileProvider.java
@@ -0,0 +1,81 @@
+// Copyright 2017 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.devtools.build.android.desugar.io;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOError;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.function.Consumer;
+import java.util.stream.Stream;
+import java.util.zip.ZipEntry;
+
+/** Input provider is a directory. */
+class DirectoryInputFileProvider implements InputFileProvider {
+
+ private final Path root;
+
+ public DirectoryInputFileProvider(Path root) {
+ this.root = root;
+ }
+
+ @Override
+ public String toString() {
+ return root.getFileName().toString();
+ }
+
+ @Override
+ public InputStream getInputStream(String filename) throws IOException {
+ return new FileInputStream(root.resolve(filename).toFile());
+ }
+
+ @Override
+ public ZipEntry getZipEntry(String filename) {
+ ZipEntry destEntry = new ZipEntry(filename);
+ destEntry.setTime(0L); // Use stable timestamp Jan 1 1980
+ return destEntry;
+ }
+
+ @Override
+ public void close() throws IOException {
+ // Nothing to close
+ }
+
+ @Override
+ public Iterator<String> iterator() {
+ final List<String> entries = new ArrayList<>();
+ try (Stream<Path> paths = Files.walk(root)) {
+ paths.forEach(
+ new Consumer<Path>() {
+ @Override
+ public void accept(Path t) {
+ if (Files.isRegularFile(t)) {
+ // Internally, we use '/' as a common package separator in filename to abstract
+ // that filename can comes from a zip or a directory.
+ entries.add(root.relativize(t).toString().replace(File.separatorChar, '/'));
+ }
+ }
+ });
+ } catch (IOException e) {
+ throw new IOError(e);
+ }
+ return entries.iterator();
+ }
+}
diff --git a/java/com/google/devtools/build/android/desugar/io/DirectoryOutputFileProvider.java b/java/com/google/devtools/build/android/desugar/io/DirectoryOutputFileProvider.java
new file mode 100644
index 0000000..f8e87cb
--- /dev/null
+++ b/java/com/google/devtools/build/android/desugar/io/DirectoryOutputFileProvider.java
@@ -0,0 +1,59 @@
+// Copyright 2017 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.devtools.build.android.desugar.io;
+
+import com.google.common.io.ByteStreams;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+/** Output provider is a directory. */
+class DirectoryOutputFileProvider implements OutputFileProvider {
+
+ private final Path root;
+
+ public DirectoryOutputFileProvider(Path root) {
+ this.root = root;
+ }
+
+ @Override
+ public void copyFrom(String filename, InputFileProvider inputFileProvider) throws IOException {
+ Path path = root.resolve(filename);
+ createParentFolder(path);
+ try (InputStream is = inputFileProvider.getInputStream(filename);
+ OutputStream os = Files.newOutputStream(path)) {
+ ByteStreams.copy(is, os);
+ }
+ }
+
+ @Override
+ public void write(String filename, byte[] content) throws IOException {
+ Path path = root.resolve(filename);
+ createParentFolder(path);
+ Files.write(path, content);
+ }
+
+ @Override
+ public void close() {
+ // Nothing to close
+ }
+
+ private void createParentFolder(Path path) throws IOException {
+ if (!Files.exists(path.getParent())) {
+ Files.createDirectories(path.getParent());
+ }
+ }
+}
diff --git a/java/com/google/devtools/build/android/desugar/io/FieldInfo.java b/java/com/google/devtools/build/android/desugar/io/FieldInfo.java
new file mode 100644
index 0000000..0b4f634
--- /dev/null
+++ b/java/com/google/devtools/build/android/desugar/io/FieldInfo.java
@@ -0,0 +1,29 @@
+// Copyright 2016 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.devtools.build.android.desugar.io;
+
+import com.google.auto.value.AutoValue;
+
+/** A value class to store the fields information. */
+@AutoValue
+public abstract class FieldInfo {
+
+ public static FieldInfo create(String owner, String name, String desc) {
+ return new AutoValue_FieldInfo(owner, name, desc);
+ }
+
+ public abstract String owner();
+ public abstract String name();
+ public abstract String desc();
+}
diff --git a/java/com/google/devtools/build/android/desugar/io/HeaderClassLoader.java b/java/com/google/devtools/build/android/desugar/io/HeaderClassLoader.java
new file mode 100644
index 0000000..f70dc0e
--- /dev/null
+++ b/java/com/google/devtools/build/android/desugar/io/HeaderClassLoader.java
@@ -0,0 +1,234 @@
+// Copyright 2016 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.devtools.build.android.desugar.io;
+
+import com.google.common.collect.ImmutableList;
+import java.io.IOError;
+import java.io.IOException;
+import java.io.InputStream;
+import org.objectweb.asm.ClassReader;
+import org.objectweb.asm.ClassVisitor;
+import org.objectweb.asm.ClassWriter;
+import org.objectweb.asm.FieldVisitor;
+import org.objectweb.asm.MethodVisitor;
+import org.objectweb.asm.Opcodes;
+
+/**
+ * Class loader that can "load" classes from header Jars. This class loader stubs in missing code
+ * attributes on the fly to make {@link ClassLoader#defineClass} happy. Classes loaded are unusable
+ * other than to resolve method references, so this class loader should only be used to process or
+ * inspect classes, not to execute their code. Also note that the resulting classes may be missing
+ * private members, which header Jars may omit.
+ *
+ * @see java.net.URLClassLoader
+ */
+public class HeaderClassLoader extends ClassLoader {
+
+ private final IndexedInputs indexedInputs;
+ private final CoreLibraryRewriter rewriter;
+
+ public HeaderClassLoader(
+ IndexedInputs indexedInputs, CoreLibraryRewriter rewriter, ClassLoader parent) {
+ super(parent);
+ this.rewriter = rewriter;
+ this.indexedInputs = indexedInputs;
+ }
+
+ @Override
+ protected Class<?> findClass(String name) throws ClassNotFoundException {
+ String filename = rewriter.unprefix(name.replace('.', '/') + ".class");
+ InputFileProvider inputFileProvider = indexedInputs.getInputFileProvider(filename);
+ if (inputFileProvider == null) {
+ throw new ClassNotFoundException("Class " + name + " not found");
+ }
+ byte[] bytecode;
+ try (InputStream content = inputFileProvider.getInputStream(filename)) {
+ ClassReader reader = rewriter.reader(content);
+ // Have ASM compute maxs so we don't need to figure out how many formal parameters there are
+ ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_MAXS);
+ ImmutableList<FieldInfo> interfaceFieldNames = getFieldsIfReaderIsInterface(reader);
+ // TODO(kmb): Consider SKIP_CODE and stubbing everything so class loader doesn't verify code
+ reader.accept(new CodeStubber(writer, interfaceFieldNames), ClassReader.SKIP_DEBUG);
+ bytecode = writer.toByteArray();
+ } catch (IOException e) {
+ throw new IOError(e);
+ }
+ return defineClass(name, bytecode, 0, bytecode.length);
+ }
+
+ /**
+ * If the {@code reader} is an interface, then extract all the declared fields in it. Otherwise,
+ * return an empty list.
+ */
+ private static ImmutableList<FieldInfo> getFieldsIfReaderIsInterface(ClassReader reader) {
+ if (BitFlags.isSet(reader.getAccess(), Opcodes.ACC_INTERFACE)) {
+ NonPrimitiveFieldCollector collector = new NonPrimitiveFieldCollector();
+ reader.accept(collector, ClassReader.SKIP_CODE | ClassReader.SKIP_DEBUG);
+ return collector.declaredNonPrimitiveFields.build();
+ }
+ return ImmutableList.of();
+ }
+
+ /** Collect the fields defined in a class. */
+ private static class NonPrimitiveFieldCollector extends ClassVisitor {
+
+ final ImmutableList.Builder<FieldInfo> declaredNonPrimitiveFields = ImmutableList.builder();
+ private String internalName;
+
+ public NonPrimitiveFieldCollector() {
+ super(Opcodes.ASM6);
+ }
+
+ @Override
+ public void visit(
+ int version,
+ int access,
+ String name,
+ String signature,
+ String superName,
+ String[] interfaces) {
+ super.visit(version, access, name, signature, superName, interfaces);
+ this.internalName = name;
+ }
+
+ @Override
+ public FieldVisitor visitField(
+ int access, String name, String desc, String signature, Object value) {
+ if (isNonPrimitiveType(desc)) {
+ declaredNonPrimitiveFields.add(FieldInfo.create(internalName, name, desc));
+ }
+ return null;
+ }
+
+ private static boolean isNonPrimitiveType(String type) {
+ char firstChar = type.charAt(0);
+ return firstChar == '[' || firstChar == 'L';
+ }
+ }
+
+
+ /**
+ * Class visitor that stubs in missing code attributes, and erases the body of the static
+ * initializer of functional interfaces if the interfaces have default methods. The erasion of the
+ * clinit is mainly because when we are desugaring lambdas, we need to load the functional
+ * interfaces via class loaders, and since the interfaces have default methods, according to the
+ * JVM spec, these interfaces will be executed. This should be prevented due to security concerns.
+ */
+ private static class CodeStubber extends ClassVisitor {
+
+ private String internalName;
+ private boolean isInterface;
+ private final ImmutableList<FieldInfo> interfaceFields;
+
+ public CodeStubber(ClassVisitor cv, ImmutableList<FieldInfo> interfaceFields) {
+ super(Opcodes.ASM6, cv);
+ this.interfaceFields = interfaceFields;
+ }
+
+ @Override
+ public void visit(
+ int version,
+ int access,
+ String name,
+ String signature,
+ String superName,
+ String[] interfaces) {
+ super.visit(version, access, name, signature, superName, interfaces);
+ isInterface = BitFlags.isSet(access, Opcodes.ACC_INTERFACE);
+ internalName = name;
+ }
+
+ @Override
+ public MethodVisitor visitMethod(
+ int access, String name, String desc, String signature, String[] exceptions) {
+ MethodVisitor dest = super.visitMethod(access, name, desc, signature, exceptions);
+ if ((access & (Opcodes.ACC_ABSTRACT | Opcodes.ACC_NATIVE)) != 0) {
+ // No need to stub out abstract or native methods
+ return dest;
+ }
+ if (isInterface && "<clinit>".equals(name)) {
+ // Delete class initializers, to avoid code gets executed when we desugar lambdas.
+ // See b/62184142
+ return new InterfaceInitializerEraser(dest, internalName, interfaceFields);
+ }
+ return new BodyStubber(dest);
+ }
+ }
+
+ /**
+ * Erase the static initializer of an interface. Given an interface with non-primitive fields,
+ * this eraser discards the original body of clinit, and initializes each non-primitive field to
+ * null
+ */
+ private static class InterfaceInitializerEraser extends MethodVisitor {
+
+ private final MethodVisitor dest;
+ private final ImmutableList<FieldInfo> interfaceFields;
+
+ public InterfaceInitializerEraser(
+ MethodVisitor mv, String internalName, ImmutableList<FieldInfo> interfaceFields) {
+ super(Opcodes.ASM6);
+ dest = mv;
+ this.interfaceFields = interfaceFields;
+ }
+
+ @Override
+ public void visitCode() {
+ dest.visitCode();
+ }
+
+ @Override
+ public void visitEnd() {
+ for (FieldInfo fieldInfo : interfaceFields) {
+ dest.visitInsn(Opcodes.ACONST_NULL);
+ dest.visitFieldInsn(
+ Opcodes.PUTSTATIC, fieldInfo.owner(), fieldInfo.name(), fieldInfo.desc());
+ }
+ dest.visitInsn(Opcodes.RETURN);
+ dest.visitMaxs(0, 0);
+ dest.visitEnd();
+ }
+ }
+
+ /** Method visitor used by {@link CodeStubber} to put code into methods without code. */
+ private static class BodyStubber extends MethodVisitor {
+
+ private static final String EXCEPTION_INTERNAL_NAME = "java/lang/UnsupportedOperationException";
+
+ private boolean hasCode = false;
+
+ public BodyStubber(MethodVisitor mv) {
+ super(Opcodes.ASM6, mv);
+ }
+
+ @Override
+ public void visitCode() {
+ hasCode = true;
+ super.visitCode();
+ }
+
+ @Override
+ public void visitEnd() {
+ if (!hasCode) {
+ super.visitTypeInsn(Opcodes.NEW, EXCEPTION_INTERNAL_NAME);
+ super.visitInsn(Opcodes.DUP);
+ super.visitMethodInsn(
+ Opcodes.INVOKESPECIAL, EXCEPTION_INTERNAL_NAME, "<init>", "()V", /*itf*/ false);
+ super.visitInsn(Opcodes.ATHROW);
+ super.visitMaxs(0, 0); // triggers computation of the actual max's
+ }
+ super.visitEnd();
+ }
+ }
+}
diff --git a/java/com/google/devtools/build/android/desugar/io/IndexedInputs.java b/java/com/google/devtools/build/android/desugar/io/IndexedInputs.java
new file mode 100644
index 0000000..8ce4b62
--- /dev/null
+++ b/java/com/google/devtools/build/android/desugar/io/IndexedInputs.java
@@ -0,0 +1,94 @@
+// Copyright 2017 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.devtools.build.android.desugar.io;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkState;
+
+import com.google.common.collect.ImmutableMap;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import javax.annotation.CheckReturnValue;
+import javax.annotation.Nullable;
+
+/**
+ * Opens the given list of input files and compute an index of all classes in them, to avoid
+ * scanning all inputs over and over for each class to load. An indexed inputs can have a parent
+ * that is firstly used when a file name is searched.
+ */
+public class IndexedInputs {
+
+ private final ImmutableMap<String, InputFileProvider> inputFiles;
+
+ /**
+ * Parent {@link IndexedInputs} to use before to search a file name into this {@link
+ * IndexedInputs}.
+ */
+ @Nullable
+ private final IndexedInputs parent;
+
+ /** Index a list of input files without a parent {@link IndexedInputs}. */
+ public IndexedInputs(List<InputFileProvider> inputProviders) {
+ this.parent = null;
+ this.inputFiles = indexInputs(inputProviders);
+ }
+
+ /**
+ * Create a new {@link IndexedInputs} with input files previously indexed and with a parent {@link
+ * IndexedInputs}.
+ */
+ private IndexedInputs(
+ ImmutableMap<String, InputFileProvider> inputFiles, IndexedInputs parentIndexedInputs) {
+ this.parent = parentIndexedInputs;
+ this.inputFiles = inputFiles;
+ }
+
+ /**
+ * Create a new {@link IndexedInputs} with input files already indexed and with a parent {@link
+ * IndexedInputs}.
+ */
+ @CheckReturnValue
+ public IndexedInputs withParent(IndexedInputs parent) {
+ checkState(this.parent == null);
+ return new IndexedInputs(this.inputFiles, parent);
+ }
+
+ @Nullable
+ public InputFileProvider getInputFileProvider(String filename) {
+ checkArgument(filename.endsWith(".class"));
+
+ if (parent != null) {
+ InputFileProvider inputFileProvider = parent.getInputFileProvider(filename);
+ if (inputFileProvider != null) {
+ return inputFileProvider;
+ }
+ }
+
+ return inputFiles.get(filename);
+ }
+
+ private ImmutableMap<String, InputFileProvider> indexInputs(
+ List<InputFileProvider> inputProviders) {
+ Map<String, InputFileProvider> indexedInputs = new HashMap<>();
+ for (InputFileProvider inputProvider : inputProviders) {
+ for (String relativePath : inputProvider) {
+ if (relativePath.endsWith(".class") && !indexedInputs.containsKey(relativePath)) {
+ indexedInputs.put(relativePath, inputProvider);
+ }
+ }
+ }
+ return ImmutableMap.copyOf(indexedInputs);
+ }
+}
diff --git a/java/com/google/devtools/build/android/desugar/io/InputFileProvider.java b/java/com/google/devtools/build/android/desugar/io/InputFileProvider.java
new file mode 100644
index 0000000..c41d018
--- /dev/null
+++ b/java/com/google/devtools/build/android/desugar/io/InputFileProvider.java
@@ -0,0 +1,49 @@
+// Copyright 2017 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.devtools.build.android.desugar.io;
+
+import com.google.errorprone.annotations.MustBeClosed;
+import java.io.Closeable;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.zip.ZipEntry;
+
+/** Input file provider allows to iterate on relative path filename of a directory or a jar file. */
+public interface InputFileProvider extends Closeable, Iterable<String> {
+
+ /**
+ * Return a ZipEntry for {@code filename}. If the provider is a {@link ZipInputFileProvider}, the
+ * method returns the existing ZipEntry in order to keep its metadata, otherwise a new one is
+ * created.
+ */
+ ZipEntry getZipEntry(String filename);
+
+ /**
+ * This method returns an input stream allowing to read the file {@code filename}, it is the
+ * responsibility of the caller to close this stream.
+ */
+ InputStream getInputStream(String filename) throws IOException;
+
+ /** Transform a Path to an InputFileProvider that needs to be closed by the caller. */
+ @MustBeClosed
+ public static InputFileProvider open(Path path) throws IOException {
+ if (Files.isDirectory(path)) {
+ return new DirectoryInputFileProvider(path);
+ } else {
+ return new ZipInputFileProvider(path);
+ }
+ }
+}
diff --git a/java/com/google/devtools/build/android/desugar/io/OutputFileProvider.java b/java/com/google/devtools/build/android/desugar/io/OutputFileProvider.java
new file mode 100644
index 0000000..e693786
--- /dev/null
+++ b/java/com/google/devtools/build/android/desugar/io/OutputFileProvider.java
@@ -0,0 +1,45 @@
+// Copyright 2017 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.devtools.build.android.desugar.io;
+
+import com.google.errorprone.annotations.MustBeClosed;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+/** Output file provider allows to write files in directory or jar files. */
+public interface OutputFileProvider extends AutoCloseable {
+
+ /** Filename to use to write out dependency metadata for later consistency checking. */
+ public static final String DESUGAR_DEPS_FILENAME = "META-INF/desugar_deps";
+
+ /**
+ * Copy {@code filename} from {@code inputFileProvider} to this output. If input file provider is
+ * a {@link ZipInputFileProvider} then the metadata of the zip entry are kept.
+ */
+ void copyFrom(String filename, InputFileProvider inputFileProvider) throws IOException;
+
+ /** Write {@code content} in {@code filename} to this output */
+ void write(String filename, byte[] content) throws IOException;
+
+ /** Transform a Path to an {@link OutputFileProvider} */
+ @MustBeClosed
+ public static OutputFileProvider create(Path path) throws IOException {
+ if (Files.isDirectory(path)) {
+ return new DirectoryOutputFileProvider(path);
+ } else {
+ return new ZipOutputFileProvider(path);
+ }
+ }
+}
diff --git a/java/com/google/devtools/build/android/desugar/io/ThrowingClassLoader.java b/java/com/google/devtools/build/android/desugar/io/ThrowingClassLoader.java
new file mode 100644
index 0000000..16f83f2
--- /dev/null
+++ b/java/com/google/devtools/build/android/desugar/io/ThrowingClassLoader.java
@@ -0,0 +1,27 @@
+// Copyright 2016 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.devtools.build.android.desugar.io;
+
+/** Class loader that throws whenever it can, for use the parent of a class loader hierarchy. */
+public class ThrowingClassLoader extends ClassLoader {
+ @Override
+ protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
+ if (name.startsWith("java.")) {
+ // Use system class loader for java. classes, since ClassLoader.defineClass gets
+ // grumpy when those don't come from the standard place.
+ return super.loadClass(name, resolve);
+ }
+ throw new ClassNotFoundException();
+ }
+} \ No newline at end of file
diff --git a/java/com/google/devtools/build/android/desugar/io/ZipInputFileProvider.java b/java/com/google/devtools/build/android/desugar/io/ZipInputFileProvider.java
new file mode 100644
index 0000000..9bd7758
--- /dev/null
+++ b/java/com/google/devtools/build/android/desugar/io/ZipInputFileProvider.java
@@ -0,0 +1,64 @@
+// Copyright 2017 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.devtools.build.android.desugar.io;
+
+import com.google.common.base.Functions;
+import com.google.common.collect.Iterators;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Path;
+import java.util.Iterator;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipFile;
+
+/** Input provider is a zip file. */
+class ZipInputFileProvider implements InputFileProvider {
+
+ private final Path root;
+
+ private final ZipFile zipFile;
+
+ public ZipInputFileProvider(Path root) throws IOException {
+ this.root = root;
+ this.zipFile = new ZipFile(root.toFile());
+ }
+
+ @Override
+ public void close() throws IOException {
+ zipFile.close();
+ }
+
+ @Override
+ public String toString() {
+ return root.getFileName().toString();
+ }
+
+ @Override
+ public ZipEntry getZipEntry(String filename) {
+ ZipEntry zipEntry = zipFile.getEntry(filename);
+ zipEntry.setCompressedSize(-1);
+ return zipEntry;
+ }
+
+ @Override
+ public InputStream getInputStream(String filename) throws IOException {
+ return zipFile.getInputStream(zipFile.getEntry(filename));
+ }
+
+ @Override
+ public Iterator<String> iterator() {
+ return Iterators.transform(
+ Iterators.forEnumeration(zipFile.entries()), Functions.toStringFunction());
+ }
+}
diff --git a/java/com/google/devtools/build/android/desugar/io/ZipOutputFileProvider.java b/java/com/google/devtools/build/android/desugar/io/ZipOutputFileProvider.java
new file mode 100644
index 0000000..36cb26d
--- /dev/null
+++ b/java/com/google/devtools/build/android/desugar/io/ZipOutputFileProvider.java
@@ -0,0 +1,78 @@
+// Copyright 2017 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.devtools.build.android.desugar.io;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+import com.google.common.io.ByteStreams;
+import java.io.BufferedOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.zip.CRC32;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipOutputStream;
+
+/** Output provider is a zip file. */
+class ZipOutputFileProvider implements OutputFileProvider {
+
+ private final ZipOutputStream out;
+
+ public ZipOutputFileProvider(Path root) throws IOException {
+ out = new ZipOutputStream(new BufferedOutputStream(Files.newOutputStream(root)));
+ }
+
+ @Override
+ public void copyFrom(String filename, InputFileProvider inputFileProvider) throws IOException {
+ // TODO(bazel-team): Avoid de- and re-compressing resource files
+ out.putNextEntry(inputFileProvider.getZipEntry(filename));
+ try (InputStream is = inputFileProvider.getInputStream(filename)) {
+ ByteStreams.copy(is, out);
+ }
+ out.closeEntry();
+ }
+
+ @Override
+ public void write(String filename, byte[] content) throws IOException {
+ checkArgument(filename.equals(DESUGAR_DEPS_FILENAME) || filename.endsWith(".class"),
+ "Expect file to be copied: %s", filename);
+ writeStoredEntry(out, filename, content);
+ }
+
+ @Override
+ public void close() throws IOException {
+ out.close();
+ }
+
+ private static void writeStoredEntry(ZipOutputStream out, String filename, byte[] content)
+ throws IOException {
+ // Need to pre-compute checksum for STORED (uncompressed) entries)
+ CRC32 checksum = new CRC32();
+ checksum.update(content);
+
+ ZipEntry result = new ZipEntry(filename);
+ result.setTime(0L); // Use stable timestamp Jan 1 1980
+ result.setCrc(checksum.getValue());
+ result.setSize(content.length);
+ result.setCompressedSize(content.length);
+ // Write uncompressed, since this is just an intermediary artifact that
+ // we will convert to .dex
+ result.setMethod(ZipEntry.STORED);
+
+ out.putNextEntry(result);
+ out.write(content);
+ out.closeEntry();
+ }
+}