diff options
author | Ivan Gavrilovic <gavra@google.com> | 2018-05-08 02:26:09 -0700 |
---|---|---|
committer | android-build-merger <android-build-merger@google.com> | 2018-05-08 02:26:09 -0700 |
commit | 2b50d295f5acc8ddf8924cd6536dfbfe45965ade (patch) | |
tree | 74deac1e16e97c2c13f226cd4635cd65abf19303 /java/com/google/devtools/build/android/desugar/io | |
parent | 301a69dfe6fbb59072b6c1af278ec31c10cbdf35 (diff) | |
parent | 6beb00b4744298d2ef28b6590c31b6848885b28d (diff) | |
download | desugar-2b50d295f5acc8ddf8924cd6536dfbfe45965ade.tar.gz |
Merge remote-tracking branch upstream-master into master am: 9d2aa11004android-o-mr1-iot-release-1.0.4android-o-mr1-iot-release-1.0.3
am: 6beb00b474
Change-Id: I5f801929d952fac02c0b652fb9003295f4bf7820
Diffstat (limited to 'java/com/google/devtools/build/android/desugar/io')
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(); + } +} |