From 44b53edaa9c5611c75e08da2fdc5d91c7b0ac6ae Mon Sep 17 00:00:00 2001 From: kmb Date: Mon, 12 Mar 2018 21:37:51 -0700 Subject: Make KeepScanner tool search classpath for nearest definition of each member reference, instead of potentially referring to a subtype. Refactor desugar's class loading machinery and related code into a separate package for easier reuse in this tool. RELNOTES: None. PiperOrigin-RevId: 188825305 GitOrigin-RevId: 2cbeb24a9c41c6b14ecbb26e2e198fbaf79aea64 Change-Id: Ie2969cb1e1c86aa68c5a6dc0be6b42b09dfaee70 --- .../devtools/build/android/desugar/BitFlags.java | 51 ----- .../android/desugar/BytecodeTypeInference.java | 1 + .../build/android/desugar/ClassReaderFactory.java | 3 + .../build/android/desugar/ClassVsInterface.java | 1 + .../build/android/desugar/CoreLibraryRewriter.java | 201 ------------------ .../build/android/desugar/CoreLibrarySupport.java | 2 + .../android/desugar/DefaultMethodClassFixer.java | 1 + .../devtools/build/android/desugar/Desugar.java | 48 +---- .../desugar/DirectoryInputFileProvider.java | 81 ------- .../desugar/DirectoryOutputFileProvider.java | 59 ------ .../android/desugar/EmulatedInterfaceRewriter.java | 1 + .../devtools/build/android/desugar/FieldInfo.java | 29 --- .../build/android/desugar/HeaderClassLoader.java | 234 --------------------- .../build/android/desugar/IndexedInputs.java | 94 --------- .../build/android/desugar/InputFileProvider.java | 36 ---- .../build/android/desugar/InterfaceDesugaring.java | 2 + .../build/android/desugar/Java7Compatibility.java | 1 + .../build/android/desugar/LambdaClassFixer.java | 1 + .../build/android/desugar/LambdaDesugaring.java | 1 + .../android/desugar/LongCompareMethodRewriter.java | 1 + .../ObjectsRequireNonNullMethodRewriter.java | 1 + .../build/android/desugar/OutputFileProvider.java | 32 --- .../android/desugar/TryWithResourcesRewriter.java | 1 + .../android/desugar/ZipInputFileProvider.java | 64 ------ .../android/desugar/ZipOutputFileProvider.java | 78 ------- .../build/android/desugar/io/BitFlags.java | 51 +++++ .../android/desugar/io/CoreLibraryRewriter.java | 201 ++++++++++++++++++ .../desugar/io/DirectoryInputFileProvider.java | 81 +++++++ .../desugar/io/DirectoryOutputFileProvider.java | 59 ++++++ .../build/android/desugar/io/FieldInfo.java | 29 +++ .../android/desugar/io/HeaderClassLoader.java | 234 +++++++++++++++++++++ .../build/android/desugar/io/IndexedInputs.java | 94 +++++++++ .../android/desugar/io/InputFileProvider.java | 49 +++++ .../android/desugar/io/OutputFileProvider.java | 45 ++++ .../android/desugar/io/ThrowingClassLoader.java | 27 +++ .../android/desugar/io/ZipInputFileProvider.java | 64 ++++++ .../android/desugar/io/ZipOutputFileProvider.java | 78 +++++++ .../build/android/desugar/scan/KeepScanner.java | 143 ++++++++++++- .../android/desugar/CoreLibrarySupportTest.java | 1 + .../android/desugar/CorePackageRenamerTest.java | 1 + .../desugar/DefaultMethodClassFixerTest.java | 5 +- .../build/android/desugar/FieldInfoTest.java | 33 --- .../build/android/desugar/IndexedInputsTest.java | 136 ------------ .../android/desugar/Java7CompatibilityTest.java | 1 + .../desugar/TryWithResourcesRewriterTest.java | 1 + .../build/android/desugar/io/FieldInfoTest.java | 33 +++ .../android/desugar/io/IndexedInputsTest.java | 136 ++++++++++++ .../scan/testdata/CollectionReferences.java | 4 + .../build/android/desugar/scan/testdata_golden.txt | 16 ++ 49 files changed, 1371 insertions(+), 1175 deletions(-) delete mode 100644 java/com/google/devtools/build/android/desugar/BitFlags.java delete mode 100644 java/com/google/devtools/build/android/desugar/CoreLibraryRewriter.java delete mode 100644 java/com/google/devtools/build/android/desugar/DirectoryInputFileProvider.java delete mode 100644 java/com/google/devtools/build/android/desugar/DirectoryOutputFileProvider.java delete mode 100644 java/com/google/devtools/build/android/desugar/FieldInfo.java delete mode 100644 java/com/google/devtools/build/android/desugar/HeaderClassLoader.java delete mode 100644 java/com/google/devtools/build/android/desugar/IndexedInputs.java delete mode 100644 java/com/google/devtools/build/android/desugar/InputFileProvider.java delete mode 100644 java/com/google/devtools/build/android/desugar/OutputFileProvider.java delete mode 100644 java/com/google/devtools/build/android/desugar/ZipInputFileProvider.java delete mode 100644 java/com/google/devtools/build/android/desugar/ZipOutputFileProvider.java create mode 100644 java/com/google/devtools/build/android/desugar/io/BitFlags.java create mode 100644 java/com/google/devtools/build/android/desugar/io/CoreLibraryRewriter.java create mode 100644 java/com/google/devtools/build/android/desugar/io/DirectoryInputFileProvider.java create mode 100644 java/com/google/devtools/build/android/desugar/io/DirectoryOutputFileProvider.java create mode 100644 java/com/google/devtools/build/android/desugar/io/FieldInfo.java create mode 100644 java/com/google/devtools/build/android/desugar/io/HeaderClassLoader.java create mode 100644 java/com/google/devtools/build/android/desugar/io/IndexedInputs.java create mode 100644 java/com/google/devtools/build/android/desugar/io/InputFileProvider.java create mode 100644 java/com/google/devtools/build/android/desugar/io/OutputFileProvider.java create mode 100644 java/com/google/devtools/build/android/desugar/io/ThrowingClassLoader.java create mode 100644 java/com/google/devtools/build/android/desugar/io/ZipInputFileProvider.java create mode 100644 java/com/google/devtools/build/android/desugar/io/ZipOutputFileProvider.java delete mode 100644 test/java/com/google/devtools/build/android/desugar/FieldInfoTest.java delete mode 100644 test/java/com/google/devtools/build/android/desugar/IndexedInputsTest.java create mode 100644 test/java/com/google/devtools/build/android/desugar/io/FieldInfoTest.java create mode 100644 test/java/com/google/devtools/build/android/desugar/io/IndexedInputsTest.java diff --git a/java/com/google/devtools/build/android/desugar/BitFlags.java b/java/com/google/devtools/build/android/desugar/BitFlags.java deleted file mode 100644 index 8be2288..0000000 --- a/java/com/google/devtools/build/android/desugar/BitFlags.java +++ /dev/null @@ -1,51 +0,0 @@ -// 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; - -import org.objectweb.asm.Opcodes; - -/** Convenience method for working with {@code int} bitwise flags. */ -class BitFlags { - - /** - * Returns {@code true} iff all 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 none 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/BytecodeTypeInference.java b/java/com/google/devtools/build/android/desugar/BytecodeTypeInference.java index 783069f..ce36071 100644 --- a/java/com/google/devtools/build/android/desugar/BytecodeTypeInference.java +++ b/java/com/google/devtools/build/android/desugar/BytecodeTypeInference.java @@ -19,6 +19,7 @@ import static com.google.common.base.Preconditions.checkState; import com.google.auto.value.AutoValue; import com.google.common.collect.ImmutableList; +import com.google.devtools.build.android.desugar.io.BitFlags; import java.util.ArrayList; import java.util.Optional; import javax.annotation.Nullable; diff --git a/java/com/google/devtools/build/android/desugar/ClassReaderFactory.java b/java/com/google/devtools/build/android/desugar/ClassReaderFactory.java index bae5251..aff9bab 100644 --- a/java/com/google/devtools/build/android/desugar/ClassReaderFactory.java +++ b/java/com/google/devtools/build/android/desugar/ClassReaderFactory.java @@ -13,6 +13,9 @@ // limitations under the License. package com.google.devtools.build.android.desugar; +import com.google.devtools.build.android.desugar.io.CoreLibraryRewriter; +import com.google.devtools.build.android.desugar.io.IndexedInputs; +import com.google.devtools.build.android.desugar.io.InputFileProvider; import java.io.IOException; import java.io.InputStream; import javax.annotation.Nullable; diff --git a/java/com/google/devtools/build/android/desugar/ClassVsInterface.java b/java/com/google/devtools/build/android/desugar/ClassVsInterface.java index cb62deb..2724454 100644 --- a/java/com/google/devtools/build/android/desugar/ClassVsInterface.java +++ b/java/com/google/devtools/build/android/desugar/ClassVsInterface.java @@ -16,6 +16,7 @@ package com.google.devtools.build.android.desugar; import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkState; +import com.google.devtools.build.android.desugar.io.BitFlags; import java.util.HashMap; import javax.annotation.Nullable; import org.objectweb.asm.ClassReader; diff --git a/java/com/google/devtools/build/android/desugar/CoreLibraryRewriter.java b/java/com/google/devtools/build/android/desugar/CoreLibraryRewriter.java deleted file mode 100644 index 698fc53..0000000 --- a/java/com/google/devtools/build/android/desugar/CoreLibraryRewriter.java +++ /dev/null @@ -1,201 +0,0 @@ -// 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; - -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 */ -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 - String getClassName() { - return finalClassName; - } - - 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/CoreLibrarySupport.java b/java/com/google/devtools/build/android/desugar/CoreLibrarySupport.java index da23c12..fd10e5e 100644 --- a/java/com/google/devtools/build/android/desugar/CoreLibrarySupport.java +++ b/java/com/google/devtools/build/android/desugar/CoreLibrarySupport.java @@ -25,6 +25,8 @@ import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.common.collect.LinkedHashMultimap; import com.google.common.collect.Multimap; +import com.google.devtools.build.android.desugar.io.BitFlags; +import com.google.devtools.build.android.desugar.io.CoreLibraryRewriter; import com.google.errorprone.annotations.Immutable; import java.lang.reflect.Method; import java.util.Collection; diff --git a/java/com/google/devtools/build/android/desugar/DefaultMethodClassFixer.java b/java/com/google/devtools/build/android/desugar/DefaultMethodClassFixer.java index 853ed09..960cfeb 100644 --- a/java/com/google/devtools/build/android/desugar/DefaultMethodClassFixer.java +++ b/java/com/google/devtools/build/android/desugar/DefaultMethodClassFixer.java @@ -18,6 +18,7 @@ import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkState; import com.google.common.collect.ImmutableList; +import com.google.devtools.build.android.desugar.io.BitFlags; import java.util.Comparator; import java.util.HashSet; import java.util.Iterator; diff --git a/java/com/google/devtools/build/android/desugar/Desugar.java b/java/com/google/devtools/build/android/desugar/Desugar.java index 506a380..c176f9c 100644 --- a/java/com/google/devtools/build/android/desugar/Desugar.java +++ b/java/com/google/devtools/build/android/desugar/Desugar.java @@ -27,14 +27,19 @@ import com.google.common.io.ByteStreams; import com.google.common.io.Closer; import com.google.devtools.build.android.Converters.ExistingPathConverter; import com.google.devtools.build.android.Converters.PathConverter; -import com.google.devtools.build.android.desugar.CoreLibraryRewriter.UnprefixingClassWriter; +import com.google.devtools.build.android.desugar.io.CoreLibraryRewriter; +import com.google.devtools.build.android.desugar.io.CoreLibraryRewriter.UnprefixingClassWriter; +import com.google.devtools.build.android.desugar.io.HeaderClassLoader; +import com.google.devtools.build.android.desugar.io.IndexedInputs; +import com.google.devtools.build.android.desugar.io.InputFileProvider; +import com.google.devtools.build.android.desugar.io.OutputFileProvider; +import com.google.devtools.build.android.desugar.io.ThrowingClassLoader; import com.google.devtools.common.options.Option; import com.google.devtools.common.options.OptionDocumentationCategory; import com.google.devtools.common.options.OptionEffectTag; import com.google.devtools.common.options.OptionsBase; import com.google.devtools.common.options.OptionsParser; import com.google.devtools.common.options.ShellQuotedParamsFilePreProcessor; -import com.google.errorprone.annotations.MustBeClosed; import java.io.IOError; import java.io.IOException; import java.io.InputStream; @@ -386,8 +391,8 @@ class Desugar { Files.isDirectory(inputPath) || !Files.isDirectory(outputPath), "Input jar file requires an output jar file"); - try (OutputFileProvider outputFileProvider = toOutputFileProvider(outputPath); - InputFileProvider inputFiles = toInputFileProvider(inputPath)) { + try (OutputFileProvider outputFileProvider = OutputFileProvider.create(outputPath); + InputFileProvider inputFiles = InputFileProvider.open(inputPath)) { DependencyCollector depsCollector = createDepsCollector(); IndexedInputs indexedInputFiles = new IndexedInputs(ImmutableList.of(inputFiles)); // Prepend classpath with input file itself so LambdaDesugaring can load classes with @@ -942,19 +947,6 @@ class Desugar { return ioPairListbuilder.build(); } - @VisibleForTesting - static 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(); - } - } - private static void deleteTreeOnExit(final Path directory) { Thread shutdownHook = new Thread() { @@ -993,26 +985,6 @@ class Desugar { } } - /** Transform a Path to an {@link OutputFileProvider} */ - @MustBeClosed - private static OutputFileProvider toOutputFileProvider(Path path) throws IOException { - if (Files.isDirectory(path)) { - return new DirectoryOutputFileProvider(path); - } else { - return new ZipOutputFileProvider(path); - } - } - - /** Transform a Path to an InputFileProvider that needs to be closed by the caller. */ - @MustBeClosed - private static InputFileProvider toInputFileProvider(Path path) throws IOException { - if (Files.isDirectory(path)) { - return new DirectoryInputFileProvider(path); - } else { - return new ZipInputFileProvider(path); - } - } - /** * Transform a list of Path to a list of InputFileProvider and register them with the given * closer. @@ -1023,7 +995,7 @@ class Desugar { Closer closer, List paths) throws IOException { ImmutableList.Builder builder = new ImmutableList.Builder<>(); for (Path path : paths) { - builder.add(closer.register(toInputFileProvider(path))); + builder.add(closer.register(InputFileProvider.open(path))); } return builder.build(); } diff --git a/java/com/google/devtools/build/android/desugar/DirectoryInputFileProvider.java b/java/com/google/devtools/build/android/desugar/DirectoryInputFileProvider.java deleted file mode 100644 index 1c5abc9..0000000 --- a/java/com/google/devtools/build/android/desugar/DirectoryInputFileProvider.java +++ /dev/null @@ -1,81 +0,0 @@ -// 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; - -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 iterator() { - final List entries = new ArrayList<>(); - try (Stream paths = Files.walk(root)) { - paths.forEach( - new Consumer() { - @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/DirectoryOutputFileProvider.java b/java/com/google/devtools/build/android/desugar/DirectoryOutputFileProvider.java deleted file mode 100644 index 782a81e..0000000 --- a/java/com/google/devtools/build/android/desugar/DirectoryOutputFileProvider.java +++ /dev/null @@ -1,59 +0,0 @@ -// 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; - -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. */ -public 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/EmulatedInterfaceRewriter.java b/java/com/google/devtools/build/android/desugar/EmulatedInterfaceRewriter.java index f066f2a..355dd97 100644 --- a/java/com/google/devtools/build/android/desugar/EmulatedInterfaceRewriter.java +++ b/java/com/google/devtools/build/android/desugar/EmulatedInterfaceRewriter.java @@ -13,6 +13,7 @@ // limitations under the License. package com.google.devtools.build.android.desugar; +import com.google.devtools.build.android.desugar.io.BitFlags; import java.util.Collections; import java.util.LinkedHashSet; import org.objectweb.asm.ClassVisitor; diff --git a/java/com/google/devtools/build/android/desugar/FieldInfo.java b/java/com/google/devtools/build/android/desugar/FieldInfo.java deleted file mode 100644 index c281039..0000000 --- a/java/com/google/devtools/build/android/desugar/FieldInfo.java +++ /dev/null @@ -1,29 +0,0 @@ -// 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; - -import com.google.auto.value.AutoValue; - -/** A value class to store the fields information. */ -@AutoValue -public abstract class FieldInfo { - - 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/HeaderClassLoader.java b/java/com/google/devtools/build/android/desugar/HeaderClassLoader.java deleted file mode 100644 index 77d99bb..0000000 --- a/java/com/google/devtools/build/android/desugar/HeaderClassLoader.java +++ /dev/null @@ -1,234 +0,0 @@ -// 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; - -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 - */ -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 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 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 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 interfaceFields; - - public CodeStubber(ClassVisitor cv, ImmutableList 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 && "".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 interfaceFields; - - public InterfaceInitializerEraser( - MethodVisitor mv, String internalName, ImmutableList 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, "", "()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/IndexedInputs.java b/java/com/google/devtools/build/android/desugar/IndexedInputs.java deleted file mode 100644 index 33c6132..0000000 --- a/java/com/google/devtools/build/android/desugar/IndexedInputs.java +++ /dev/null @@ -1,94 +0,0 @@ -// 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; - -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. - */ -class IndexedInputs { - - private final ImmutableMap 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 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 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 indexInputs( - List inputProviders) { - Map 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/InputFileProvider.java b/java/com/google/devtools/build/android/desugar/InputFileProvider.java deleted file mode 100644 index c2b6353..0000000 --- a/java/com/google/devtools/build/android/desugar/InputFileProvider.java +++ /dev/null @@ -1,36 +0,0 @@ -// 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; - -import java.io.Closeable; -import java.io.IOException; -import java.io.InputStream; -import java.util.zip.ZipEntry; - -/** Input file provider allows to iterate on relative path filename of a directory or a jar file. */ -interface InputFileProvider extends Closeable, Iterable { - - /** - * 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; -} diff --git a/java/com/google/devtools/build/android/desugar/InterfaceDesugaring.java b/java/com/google/devtools/build/android/desugar/InterfaceDesugaring.java index 0a10df1..e9e3199 100644 --- a/java/com/google/devtools/build/android/desugar/InterfaceDesugaring.java +++ b/java/com/google/devtools/build/android/desugar/InterfaceDesugaring.java @@ -17,6 +17,8 @@ import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkState; +import com.google.devtools.build.android.desugar.io.BitFlags; +import com.google.devtools.build.android.desugar.io.FieldInfo; import java.lang.reflect.Method; import javax.annotation.Nullable; import org.objectweb.asm.AnnotationVisitor; diff --git a/java/com/google/devtools/build/android/desugar/Java7Compatibility.java b/java/com/google/devtools/build/android/desugar/Java7Compatibility.java index 37a45dd..2090d5c 100644 --- a/java/com/google/devtools/build/android/desugar/Java7Compatibility.java +++ b/java/com/google/devtools/build/android/desugar/Java7Compatibility.java @@ -17,6 +17,7 @@ import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkState; +import com.google.devtools.build.android.desugar.io.BitFlags; import javax.annotation.Nullable; import org.objectweb.asm.AnnotationVisitor; import org.objectweb.asm.Attribute; diff --git a/java/com/google/devtools/build/android/desugar/LambdaClassFixer.java b/java/com/google/devtools/build/android/desugar/LambdaClassFixer.java index fb05bcb..6b0a921 100644 --- a/java/com/google/devtools/build/android/desugar/LambdaClassFixer.java +++ b/java/com/google/devtools/build/android/desugar/LambdaClassFixer.java @@ -19,6 +19,7 @@ import static com.google.common.base.Preconditions.checkState; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; +import com.google.devtools.build.android.desugar.io.BitFlags; import java.util.HashSet; import java.util.LinkedHashSet; import org.objectweb.asm.AnnotationVisitor; diff --git a/java/com/google/devtools/build/android/desugar/LambdaDesugaring.java b/java/com/google/devtools/build/android/desugar/LambdaDesugaring.java index 5f41347..f9b5316 100644 --- a/java/com/google/devtools/build/android/desugar/LambdaDesugaring.java +++ b/java/com/google/devtools/build/android/desugar/LambdaDesugaring.java @@ -21,6 +21,7 @@ import static org.objectweb.asm.Opcodes.ASM6; import com.google.auto.value.AutoValue; import com.google.common.collect.ImmutableSet; +import com.google.devtools.build.android.desugar.io.BitFlags; import java.io.IOException; import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandles.Lookup; diff --git a/java/com/google/devtools/build/android/desugar/LongCompareMethodRewriter.java b/java/com/google/devtools/build/android/desugar/LongCompareMethodRewriter.java index 6ac415d..7f2f355 100644 --- a/java/com/google/devtools/build/android/desugar/LongCompareMethodRewriter.java +++ b/java/com/google/devtools/build/android/desugar/LongCompareMethodRewriter.java @@ -17,6 +17,7 @@ import static org.objectweb.asm.Opcodes.ASM6; import static org.objectweb.asm.Opcodes.INVOKESTATIC; import static org.objectweb.asm.Opcodes.LCMP; +import com.google.devtools.build.android.desugar.io.CoreLibraryRewriter; import org.objectweb.asm.ClassVisitor; import org.objectweb.asm.MethodVisitor; diff --git a/java/com/google/devtools/build/android/desugar/ObjectsRequireNonNullMethodRewriter.java b/java/com/google/devtools/build/android/desugar/ObjectsRequireNonNullMethodRewriter.java index 5e0a344..931459a 100644 --- a/java/com/google/devtools/build/android/desugar/ObjectsRequireNonNullMethodRewriter.java +++ b/java/com/google/devtools/build/android/desugar/ObjectsRequireNonNullMethodRewriter.java @@ -19,6 +19,7 @@ import static org.objectweb.asm.Opcodes.INVOKESTATIC; import static org.objectweb.asm.Opcodes.INVOKEVIRTUAL; import static org.objectweb.asm.Opcodes.POP; +import com.google.devtools.build.android.desugar.io.CoreLibraryRewriter; import org.objectweb.asm.ClassVisitor; import org.objectweb.asm.MethodVisitor; diff --git a/java/com/google/devtools/build/android/desugar/OutputFileProvider.java b/java/com/google/devtools/build/android/desugar/OutputFileProvider.java deleted file mode 100644 index 7a590ef..0000000 --- a/java/com/google/devtools/build/android/desugar/OutputFileProvider.java +++ /dev/null @@ -1,32 +0,0 @@ -// 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; - -import java.io.IOException; - -/** Output file provider allows to write files in directory or jar files. */ -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; -} diff --git a/java/com/google/devtools/build/android/desugar/TryWithResourcesRewriter.java b/java/com/google/devtools/build/android/desugar/TryWithResourcesRewriter.java index e8509e7..818585f 100644 --- a/java/com/google/devtools/build/android/desugar/TryWithResourcesRewriter.java +++ b/java/com/google/devtools/build/android/desugar/TryWithResourcesRewriter.java @@ -29,6 +29,7 @@ import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableMultimap; import com.google.common.collect.ImmutableSet; import com.google.devtools.build.android.desugar.BytecodeTypeInference.InferredType; +import com.google.devtools.build.android.desugar.io.BitFlags; import java.util.Collections; import java.util.LinkedHashSet; import java.util.Optional; diff --git a/java/com/google/devtools/build/android/desugar/ZipInputFileProvider.java b/java/com/google/devtools/build/android/desugar/ZipInputFileProvider.java deleted file mode 100644 index 307c8b8..0000000 --- a/java/com/google/devtools/build/android/desugar/ZipInputFileProvider.java +++ /dev/null @@ -1,64 +0,0 @@ -// 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; - -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 iterator() { - return Iterators.transform( - Iterators.forEnumeration(zipFile.entries()), Functions.toStringFunction()); - } -} diff --git a/java/com/google/devtools/build/android/desugar/ZipOutputFileProvider.java b/java/com/google/devtools/build/android/desugar/ZipOutputFileProvider.java deleted file mode 100644 index 8d6501d..0000000 --- a/java/com/google/devtools/build/android/desugar/ZipOutputFileProvider.java +++ /dev/null @@ -1,78 +0,0 @@ -// 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; - -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. */ -public 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(); - } -} 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 all 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 none 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 iterator() { + final List entries = new ArrayList<>(); + try (Stream paths = Files.walk(root)) { + paths.forEach( + new Consumer() { + @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 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 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 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 interfaceFields; + + public CodeStubber(ClassVisitor cv, ImmutableList 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 && "".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 interfaceFields; + + public InterfaceInitializerEraser( + MethodVisitor mv, String internalName, ImmutableList 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, "", "()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 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 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 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 indexInputs( + List inputProviders) { + Map 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 { + + /** + * 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 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(); + } +} diff --git a/java/com/google/devtools/build/android/desugar/scan/KeepScanner.java b/java/com/google/devtools/build/android/desugar/scan/KeepScanner.java index b347c7a..4924f7c 100644 --- a/java/com/google/devtools/build/android/desugar/scan/KeepScanner.java +++ b/java/com/google/devtools/build/android/desugar/scan/KeepScanner.java @@ -15,13 +15,21 @@ package com.google.devtools.build.android.desugar.scan; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; import static java.nio.file.StandardOpenOption.CREATE; import static java.util.Comparator.comparing; +import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; import com.google.common.io.ByteStreams; +import com.google.common.io.Closer; import com.google.devtools.build.android.Converters.ExistingPathConverter; import com.google.devtools.build.android.Converters.PathConverter; +import com.google.devtools.build.android.desugar.io.CoreLibraryRewriter; +import com.google.devtools.build.android.desugar.io.HeaderClassLoader; +import com.google.devtools.build.android.desugar.io.IndexedInputs; +import com.google.devtools.build.android.desugar.io.InputFileProvider; +import com.google.devtools.build.android.desugar.io.ThrowingClassLoader; import com.google.devtools.common.options.Option; import com.google.devtools.common.options.OptionDocumentationCategory; import com.google.devtools.common.options.OptionEffectTag; @@ -32,9 +40,11 @@ import java.io.IOError; import java.io.IOException; import java.io.InputStream; import java.io.PrintStream; +import java.lang.reflect.Method; import java.nio.file.FileSystems; import java.nio.file.Files; import java.nio.file.Path; +import java.util.List; import java.util.Map; import java.util.stream.Collectors; import java.util.zip.ZipEntry; @@ -56,6 +66,32 @@ class KeepScanner { ) public Path inputJars; + @Option( + name = "classpath_entry", + allowMultiple = true, + defaultValue = "", + documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, + effectTags = {OptionEffectTag.UNKNOWN}, + converter = ExistingPathConverter.class, + help = + "Ordered classpath (Jar or directory) to resolve symbols in the --input Jar, like " + + "javac's -cp flag." + ) + public List classpath; + + @Option( + name = "bootclasspath_entry", + allowMultiple = true, + defaultValue = "", + documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, + effectTags = {OptionEffectTag.UNKNOWN}, + converter = ExistingPathConverter.class, + help = + "Bootclasspath that was used to compile the --input Jar with, like javac's " + + "-bootclasspath flag (required)." + ) + public List bootclasspath; + @Option( name = "keep_file", defaultValue = "null", @@ -81,10 +117,25 @@ class KeepScanner { parser.setAllowResidue(false); parser.enableParamsFileSupport(new ShellQuotedParamsFilePreProcessor(FileSystems.getDefault())); parser.parseAndExitUponError(args); - KeepScannerOptions options = parser.getOptions(KeepScannerOptions.class); - Map> seeds = - scan(checkNotNull(options.inputJars), options.prefix); + + Map> seeds; + try (Closer closer = Closer.create()) { + // TODO(kmb): Try to share more of this code with Desugar binary + IndexedInputs classpath = + new IndexedInputs(toRegisteredInputFileProvider(closer, options.classpath)); + IndexedInputs bootclasspath = + new IndexedInputs(toRegisteredInputFileProvider(closer, options.bootclasspath)); + + // Construct classloader from classpath. Since we're assuming the prefix we're looking for + // isn't part of the input itself we shouldn't need to include the input in the classloader. + CoreLibraryRewriter noopRewriter = new CoreLibraryRewriter(""); + ClassLoader classloader = + new HeaderClassLoader(classpath, noopRewriter, + new HeaderClassLoader(bootclasspath, noopRewriter, + new ThrowingClassLoader())); + seeds = scan(checkNotNull(options.inputJars), options.prefix, classloader); + } try (PrintStream out = new PrintStream( @@ -117,11 +168,9 @@ class KeepScanner { }); } - /** - * Scans for and returns references with owners matching the given prefix grouped by owner. - */ - private static Map> scan(Path jarFile, String prefix) - throws IOException { + /** Scans for and returns references with owners matching the given prefix grouped by owner. */ + private static Map> scan( + Path jarFile, String prefix, ClassLoader classpath) throws IOException { // We read the Jar sequentially since ZipFile uses locks anyway but then allow scanning each // class in parallel. try (ZipFile zip = new ZipFile(jarFile.toFile())) { @@ -131,6 +180,8 @@ class KeepScanner { .parallel() .flatMap( content -> PrefixReferenceScanner.scan(new ClassReader(content), prefix).stream()) + .distinct() // so we don't process the same reference multiple times next + .map(ref -> nearestDeclaration(ref, classpath)) .collect( Collectors.groupingByConcurrent( KeepReference::internalName, ImmutableSet.toImmutableSet())); @@ -147,6 +198,68 @@ class KeepScanner { } } + /** + * Find the nearest definition of the given reference in the class hierarchy and return the + * modified reference. This is needed b/c bytecode sometimes refers to a method or field using + * an owner type that inherits the method or field instead of defining the member itself. + * In that case we need to find and keep the inherited definition. + */ + private static KeepReference nearestDeclaration(KeepReference ref, ClassLoader classpath) { + if (!ref.isMemberReference() || "".equals(ref.name())) { + return ref; // class and constructor references don't need any further work + } + + Class clazz; + try { + clazz = classpath.loadClass(ref.internalName().replace('/', '.')); + } catch (ClassNotFoundException e) { + throw (NoClassDefFoundError) new NoClassDefFoundError("Couldn't load " + ref).initCause(e); + } + + Class owner = findDeclaringClass(clazz, ref); + if (owner == clazz) { + return ref; + } + String parent = checkNotNull(owner, "Can't resolve: %s", ref).getName().replace('.', '/'); + return KeepReference.memberReference(parent, ref.name(), ref.desc()); + } + + private static Class findDeclaringClass(Class clazz, KeepReference ref) { + if (ref.isFieldReference()) { + try { + return clazz.getField(ref.name()).getDeclaringClass(); + } catch (NoSuchFieldException e) { + // field must be non-public, so search class hierarchy + do { + try { + return clazz.getDeclaredField(ref.name()).getDeclaringClass(); + } catch (NoSuchFieldException ignored) { + // fall through for clarity + } + clazz = clazz.getSuperclass(); + } while (clazz != null); + } + } else { + checkState(ref.isMethodReference()); + Type descriptor = Type.getMethodType(ref.desc()); + for (Method m : clazz.getMethods()) { + if (m.getName().equals(ref.name()) && Type.getType(m).equals(descriptor)) { + return m.getDeclaringClass(); + } + } + do { + // Method must be non-public, so search class hierarchy + for (Method m : clazz.getDeclaredMethods()) { + if (m.getName().equals(ref.name()) && Type.getType(m).equals(descriptor)) { + return m.getDeclaringClass(); + } + } + clazz = clazz.getSuperclass(); + } while (clazz != null); + } + return null; + } + private static CharSequence toKeepDescriptor(KeepReference member) { StringBuilder result = new StringBuilder(); if (member.isMethodReference()) { @@ -172,5 +285,19 @@ class KeepScanner { return result; } + /** + * Transform a list of Path to a list of InputFileProvider and register them with the given + * closer. + */ + @SuppressWarnings("MustBeClosedChecker") + private static ImmutableList toRegisteredInputFileProvider( + Closer closer, List paths) throws IOException { + ImmutableList.Builder builder = new ImmutableList.Builder<>(); + for (Path path : paths) { + builder.add(closer.register(InputFileProvider.open(path))); + } + return builder.build(); + } + private KeepScanner() {} } diff --git a/test/java/com/google/devtools/build/android/desugar/CoreLibrarySupportTest.java b/test/java/com/google/devtools/build/android/desugar/CoreLibrarySupportTest.java index ec4e16d..42f1f78 100644 --- a/test/java/com/google/devtools/build/android/desugar/CoreLibrarySupportTest.java +++ b/test/java/com/google/devtools/build/android/desugar/CoreLibrarySupportTest.java @@ -16,6 +16,7 @@ package com.google.devtools.build.android.desugar; import static com.google.common.truth.Truth.assertThat; import com.google.common.collect.ImmutableList; +import com.google.devtools.build.android.desugar.io.CoreLibraryRewriter; import java.util.Collection; import java.util.Comparator; import java.util.Map; diff --git a/test/java/com/google/devtools/build/android/desugar/CorePackageRenamerTest.java b/test/java/com/google/devtools/build/android/desugar/CorePackageRenamerTest.java index 2bdd58b..5220ed6 100644 --- a/test/java/com/google/devtools/build/android/desugar/CorePackageRenamerTest.java +++ b/test/java/com/google/devtools/build/android/desugar/CorePackageRenamerTest.java @@ -16,6 +16,7 @@ package com.google.devtools.build.android.desugar; import static com.google.common.truth.Truth.assertThat; import com.google.common.collect.ImmutableList; +import com.google.devtools.build.android.desugar.io.CoreLibraryRewriter; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; diff --git a/test/java/com/google/devtools/build/android/desugar/DefaultMethodClassFixerTest.java b/test/java/com/google/devtools/build/android/desugar/DefaultMethodClassFixerTest.java index faa6dda..406a36f 100644 --- a/test/java/com/google/devtools/build/android/desugar/DefaultMethodClassFixerTest.java +++ b/test/java/com/google/devtools/build/android/desugar/DefaultMethodClassFixerTest.java @@ -19,7 +19,10 @@ import static com.google.devtools.build.android.desugar.DefaultMethodClassFixer. import com.google.common.collect.ImmutableList; import com.google.common.io.Closer; -import com.google.devtools.build.android.desugar.Desugar.ThrowingClassLoader; +import com.google.devtools.build.android.desugar.io.CoreLibraryRewriter; +import com.google.devtools.build.android.desugar.io.HeaderClassLoader; +import com.google.devtools.build.android.desugar.io.IndexedInputs; +import com.google.devtools.build.android.desugar.io.ThrowingClassLoader; import java.io.File; import java.io.IOException; import java.io.Serializable; diff --git a/test/java/com/google/devtools/build/android/desugar/FieldInfoTest.java b/test/java/com/google/devtools/build/android/desugar/FieldInfoTest.java deleted file mode 100644 index afb2bea..0000000 --- a/test/java/com/google/devtools/build/android/desugar/FieldInfoTest.java +++ /dev/null @@ -1,33 +0,0 @@ -// 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; - -import static com.google.common.truth.Truth.assertThat; - -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.JUnit4; - -/** Test for {@link FieldInfo} */ -@RunWith(JUnit4.class) -public class FieldInfoTest { - - @Test - public void testFieldsAreCorrectlySet() { - FieldInfo info = FieldInfo.create("owner", "name", "desc"); - assertThat(info.owner()).isEqualTo("owner"); - assertThat(info.name()).isEqualTo("name"); - assertThat(info.desc()).isEqualTo("desc"); - } -} diff --git a/test/java/com/google/devtools/build/android/desugar/IndexedInputsTest.java b/test/java/com/google/devtools/build/android/desugar/IndexedInputsTest.java deleted file mode 100644 index bac3fc9..0000000 --- a/test/java/com/google/devtools/build/android/desugar/IndexedInputsTest.java +++ /dev/null @@ -1,136 +0,0 @@ -// 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; - -import static com.google.common.truth.Truth.assertThat; - -import com.google.common.collect.ImmutableList; -import java.io.File; -import java.io.FileOutputStream; -import java.util.zip.ZipEntry; -import java.util.zip.ZipOutputStream; -import org.junit.After; -import org.junit.AfterClass; -import org.junit.Before; -import org.junit.BeforeClass; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.JUnit4; - -/** - * Test that exercises the behavior of the IndexedInputs class. - */ -@RunWith(JUnit4.class) -public final class IndexedInputsTest { - - private static File lib1; - - private static String lib1Name; - - private static File lib2; - - private static String lib2Name; - - private static InputFileProvider lib1InputFileProvider; - - private static InputFileProvider lib2InputFileProvider; - - @BeforeClass - public static void setUpClass() throws Exception { - lib1 = File.createTempFile("lib1", ".jar"); - lib1Name = lib1.getName(); - try (ZipOutputStream zos1 = new ZipOutputStream(new FileOutputStream(lib1))) { - zos1.putNextEntry(new ZipEntry("a/b/C.class")); - zos1.putNextEntry(new ZipEntry("a/b/D.class")); - zos1.closeEntry(); - } - - lib2 = File.createTempFile("lib2", ".jar"); - lib2Name = lib2.getName(); - try (ZipOutputStream zos2 = new ZipOutputStream(new FileOutputStream(lib2))) { - zos2.putNextEntry(new ZipEntry("a/b/C.class")); - zos2.putNextEntry(new ZipEntry("a/b/E.class")); - zos2.closeEntry(); - } - } - - @Before - public void createProviders() throws Exception { - lib1InputFileProvider = new ZipInputFileProvider(lib1.toPath()); - lib2InputFileProvider = new ZipInputFileProvider(lib2.toPath()); - } - - @After - public void closeProviders() throws Exception { - lib1InputFileProvider.close(); - lib2InputFileProvider.close(); - } - - @AfterClass - public static void tearDownClass() throws Exception { - lib1.delete(); - lib2.delete(); - } - - @Test - public void testClassFoundWithParentLibrary() throws Exception { - IndexedInputs indexedLib2 = new IndexedInputs(ImmutableList.of(lib2InputFileProvider)); - IndexedInputs indexedLib1 = new IndexedInputs(ImmutableList.of(lib1InputFileProvider)); - IndexedInputs indexedLib2AndLib1 = indexedLib1.withParent(indexedLib2); - assertThat(indexedLib2AndLib1.getInputFileProvider("a/b/C.class").toString()) - .isEqualTo(lib2Name); - assertThat(indexedLib2AndLib1.getInputFileProvider("a/b/D.class").toString()) - .isEqualTo(lib1Name); - assertThat(indexedLib2AndLib1.getInputFileProvider("a/b/E.class").toString()) - .isEqualTo(lib2Name); - - indexedLib2 = new IndexedInputs(ImmutableList.of(lib2InputFileProvider)); - indexedLib1 = new IndexedInputs(ImmutableList.of(lib1InputFileProvider)); - IndexedInputs indexedLib1AndLib2 = indexedLib2.withParent(indexedLib1); - assertThat(indexedLib1AndLib2.getInputFileProvider("a/b/C.class").toString()) - .isEqualTo(lib1Name); - assertThat(indexedLib1AndLib2.getInputFileProvider("a/b/D.class").toString()) - .isEqualTo(lib1Name); - assertThat(indexedLib1AndLib2.getInputFileProvider("a/b/E.class").toString()) - .isEqualTo(lib2Name); - } - - @Test - public void testClassFoundWithoutParentLibrary() throws Exception { - IndexedInputs ijLib1Lib2 = - new IndexedInputs(ImmutableList.of(lib1InputFileProvider, lib2InputFileProvider)); - assertThat(ijLib1Lib2.getInputFileProvider("a/b/C.class").toString()) - .isEqualTo(lib1Name); - assertThat(ijLib1Lib2.getInputFileProvider("a/b/D.class").toString()) - .isEqualTo(lib1Name); - assertThat(ijLib1Lib2.getInputFileProvider("a/b/E.class").toString()) - .isEqualTo(lib2Name); - - IndexedInputs ijLib2Lib1 = - new IndexedInputs(ImmutableList.of(lib2InputFileProvider, lib1InputFileProvider)); - assertThat(ijLib2Lib1.getInputFileProvider("a/b/C.class").toString()) - .isEqualTo(lib2Name); - assertThat(ijLib2Lib1.getInputFileProvider("a/b/D.class").toString()) - .isEqualTo(lib1Name); - assertThat(ijLib2Lib1.getInputFileProvider("a/b/E.class").toString()) - .isEqualTo(lib2Name); - } - - @Test - public void testClassNotFound() throws Exception { - IndexedInputs ijLib1Lib2 = - new IndexedInputs(ImmutableList.of(lib1InputFileProvider, lib2InputFileProvider)); - assertThat(ijLib1Lib2.getInputFileProvider("a/b/F.class")).isNull(); - } -} diff --git a/test/java/com/google/devtools/build/android/desugar/Java7CompatibilityTest.java b/test/java/com/google/devtools/build/android/desugar/Java7CompatibilityTest.java index 2eab943..99e51c1 100644 --- a/test/java/com/google/devtools/build/android/desugar/Java7CompatibilityTest.java +++ b/test/java/com/google/devtools/build/android/desugar/Java7CompatibilityTest.java @@ -16,6 +16,7 @@ package com.google.devtools.build.android.desugar; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.fail; +import com.google.devtools.build.android.desugar.io.BitFlags; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; diff --git a/test/java/com/google/devtools/build/android/desugar/TryWithResourcesRewriterTest.java b/test/java/com/google/devtools/build/android/desugar/TryWithResourcesRewriterTest.java index 37afae7..dc0da22 100644 --- a/test/java/com/google/devtools/build/android/desugar/TryWithResourcesRewriterTest.java +++ b/test/java/com/google/devtools/build/android/desugar/TryWithResourcesRewriterTest.java @@ -25,6 +25,7 @@ import static org.objectweb.asm.Opcodes.ASM5; import static org.objectweb.asm.Opcodes.INVOKESTATIC; import static org.objectweb.asm.Opcodes.INVOKEVIRTUAL; +import com.google.devtools.build.android.desugar.io.BitFlags; import com.google.devtools.build.android.desugar.runtime.ThrowableExtension; import com.google.devtools.build.android.desugar.testdata.ClassUsingTryWithResources; import java.io.IOException; diff --git a/test/java/com/google/devtools/build/android/desugar/io/FieldInfoTest.java b/test/java/com/google/devtools/build/android/desugar/io/FieldInfoTest.java new file mode 100644 index 0000000..0579822 --- /dev/null +++ b/test/java/com/google/devtools/build/android/desugar/io/FieldInfoTest.java @@ -0,0 +1,33 @@ +// 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 static com.google.common.truth.Truth.assertThat; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Test for {@link FieldInfo} */ +@RunWith(JUnit4.class) +public class FieldInfoTest { + + @Test + public void testFieldsAreCorrectlySet() { + FieldInfo info = FieldInfo.create("owner", "name", "desc"); + assertThat(info.owner()).isEqualTo("owner"); + assertThat(info.name()).isEqualTo("name"); + assertThat(info.desc()).isEqualTo("desc"); + } +} diff --git a/test/java/com/google/devtools/build/android/desugar/io/IndexedInputsTest.java b/test/java/com/google/devtools/build/android/desugar/io/IndexedInputsTest.java new file mode 100644 index 0000000..81a4b31 --- /dev/null +++ b/test/java/com/google/devtools/build/android/desugar/io/IndexedInputsTest.java @@ -0,0 +1,136 @@ +// 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.truth.Truth.assertThat; + +import com.google.common.collect.ImmutableList; +import java.io.File; +import java.io.FileOutputStream; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; +import org.junit.After; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** + * Test that exercises the behavior of the IndexedInputs class. + */ +@RunWith(JUnit4.class) +public final class IndexedInputsTest { + + private static File lib1; + + private static String lib1Name; + + private static File lib2; + + private static String lib2Name; + + private static InputFileProvider lib1InputFileProvider; + + private static InputFileProvider lib2InputFileProvider; + + @BeforeClass + public static void setUpClass() throws Exception { + lib1 = File.createTempFile("lib1", ".jar"); + lib1Name = lib1.getName(); + try (ZipOutputStream zos1 = new ZipOutputStream(new FileOutputStream(lib1))) { + zos1.putNextEntry(new ZipEntry("a/b/C.class")); + zos1.putNextEntry(new ZipEntry("a/b/D.class")); + zos1.closeEntry(); + } + + lib2 = File.createTempFile("lib2", ".jar"); + lib2Name = lib2.getName(); + try (ZipOutputStream zos2 = new ZipOutputStream(new FileOutputStream(lib2))) { + zos2.putNextEntry(new ZipEntry("a/b/C.class")); + zos2.putNextEntry(new ZipEntry("a/b/E.class")); + zos2.closeEntry(); + } + } + + @Before + public void createProviders() throws Exception { + lib1InputFileProvider = new ZipInputFileProvider(lib1.toPath()); + lib2InputFileProvider = new ZipInputFileProvider(lib2.toPath()); + } + + @After + public void closeProviders() throws Exception { + lib1InputFileProvider.close(); + lib2InputFileProvider.close(); + } + + @AfterClass + public static void tearDownClass() throws Exception { + lib1.delete(); + lib2.delete(); + } + + @Test + public void testClassFoundWithParentLibrary() throws Exception { + IndexedInputs indexedLib2 = new IndexedInputs(ImmutableList.of(lib2InputFileProvider)); + IndexedInputs indexedLib1 = new IndexedInputs(ImmutableList.of(lib1InputFileProvider)); + IndexedInputs indexedLib2AndLib1 = indexedLib1.withParent(indexedLib2); + assertThat(indexedLib2AndLib1.getInputFileProvider("a/b/C.class").toString()) + .isEqualTo(lib2Name); + assertThat(indexedLib2AndLib1.getInputFileProvider("a/b/D.class").toString()) + .isEqualTo(lib1Name); + assertThat(indexedLib2AndLib1.getInputFileProvider("a/b/E.class").toString()) + .isEqualTo(lib2Name); + + indexedLib2 = new IndexedInputs(ImmutableList.of(lib2InputFileProvider)); + indexedLib1 = new IndexedInputs(ImmutableList.of(lib1InputFileProvider)); + IndexedInputs indexedLib1AndLib2 = indexedLib2.withParent(indexedLib1); + assertThat(indexedLib1AndLib2.getInputFileProvider("a/b/C.class").toString()) + .isEqualTo(lib1Name); + assertThat(indexedLib1AndLib2.getInputFileProvider("a/b/D.class").toString()) + .isEqualTo(lib1Name); + assertThat(indexedLib1AndLib2.getInputFileProvider("a/b/E.class").toString()) + .isEqualTo(lib2Name); + } + + @Test + public void testClassFoundWithoutParentLibrary() throws Exception { + IndexedInputs ijLib1Lib2 = + new IndexedInputs(ImmutableList.of(lib1InputFileProvider, lib2InputFileProvider)); + assertThat(ijLib1Lib2.getInputFileProvider("a/b/C.class").toString()) + .isEqualTo(lib1Name); + assertThat(ijLib1Lib2.getInputFileProvider("a/b/D.class").toString()) + .isEqualTo(lib1Name); + assertThat(ijLib1Lib2.getInputFileProvider("a/b/E.class").toString()) + .isEqualTo(lib2Name); + + IndexedInputs ijLib2Lib1 = + new IndexedInputs(ImmutableList.of(lib2InputFileProvider, lib1InputFileProvider)); + assertThat(ijLib2Lib1.getInputFileProvider("a/b/C.class").toString()) + .isEqualTo(lib2Name); + assertThat(ijLib2Lib1.getInputFileProvider("a/b/D.class").toString()) + .isEqualTo(lib1Name); + assertThat(ijLib2Lib1.getInputFileProvider("a/b/E.class").toString()) + .isEqualTo(lib2Name); + } + + @Test + public void testClassNotFound() throws Exception { + IndexedInputs ijLib1Lib2 = + new IndexedInputs(ImmutableList.of(lib1InputFileProvider, lib2InputFileProvider)); + assertThat(ijLib1Lib2.getInputFileProvider("a/b/F.class")).isNull(); + } +} diff --git a/test/java/com/google/devtools/build/android/desugar/scan/testdata/CollectionReferences.java b/test/java/com/google/devtools/build/android/desugar/scan/testdata/CollectionReferences.java index 482c32a..830364c 100644 --- a/test/java/com/google/devtools/build/android/desugar/scan/testdata/CollectionReferences.java +++ b/test/java/com/google/devtools/build/android/desugar/scan/testdata/CollectionReferences.java @@ -54,6 +54,10 @@ public class CollectionReferences { return result; } + public void expire(long before) { + dates.removeIf(d -> d.getTime() < before); + } + static { System.out.println("Hello!"); } diff --git a/test/java/com/google/devtools/build/android/desugar/scan/testdata_golden.txt b/test/java/com/google/devtools/build/android/desugar/scan/testdata_golden.txt index 35744ce..6082576 100644 --- a/test/java/com/google/devtools/build/android/desugar/scan/testdata_golden.txt +++ b/test/java/com/google/devtools/build/android/desugar/scan/testdata_golden.txt @@ -18,6 +18,19 @@ -keep class java.lang.System { *** out; } +-keep class java.lang.invoke.CallSite { +} +-keep class java.lang.invoke.LambdaMetafactory { + *** metafactory(java.lang.invoke.MethodHandles$Lookup, java.lang.String, java.lang.invoke.MethodType, java.lang.invoke.MethodType, java.lang.invoke.MethodHandle, java.lang.invoke.MethodType); +} +-keep class java.lang.invoke.MethodHandle { +} +-keep class java.lang.invoke.MethodHandles { +} +-keep class java.lang.invoke.MethodHandles$Lookup { +} +-keep class java.lang.invoke.MethodType { +} -keep class java.util.AbstractList { } -keep class java.util.ArrayList { @@ -28,6 +41,7 @@ *** iterator(); } -keep class java.util.Collection { + *** removeIf(java.util.function.Predicate); } -keep class java.util.Date { (long); @@ -44,3 +58,5 @@ *** get(int); *** iterator(); } +-keep class java.util.function.Predicate { +} -- cgit v1.2.3