diff options
author | kmb <kmb@google.com> | 2018-02-20 15:30:22 -0800 |
---|---|---|
committer | Copybara-Service <copybara-piper@google.com> | 2018-02-20 15:32:30 -0800 |
commit | c1042f2adc55d040495a1159100146fad607d32a (patch) | |
tree | 69b2e5505ba3063c6a2966c414da1067b61782b7 /src/tools/android/java/com/google/devtools/build/android/desugar | |
parent | 2dd46e6178a7e99e38c56a0fddc1a6e533b53bf0 (diff) |
Tool that scans a given Jar for references to select classes and outputs corresponding Proguard-style -keep rules
RELNOTES: None.
PiperOrigin-RevId: 186372769
Diffstat (limited to 'src/tools/android/java/com/google/devtools/build/android/desugar')
3 files changed, 630 insertions, 0 deletions
diff --git a/src/tools/android/java/com/google/devtools/build/android/desugar/scan/KeepReference.java b/src/tools/android/java/com/google/devtools/build/android/desugar/scan/KeepReference.java new file mode 100644 index 0000000000..bae3f38db8 --- /dev/null +++ b/src/tools/android/java/com/google/devtools/build/android/desugar/scan/KeepReference.java @@ -0,0 +1,51 @@ +// Copyright 2018 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.scan; + +import static com.google.common.base.Preconditions.checkArgument; + +import com.google.auto.value.AutoValue; +import com.google.errorprone.annotations.Immutable; + +@AutoValue +@Immutable +abstract class KeepReference { + public static KeepReference classReference(String internalName) { + checkArgument(!internalName.isEmpty()); + return new AutoValue_KeepReference(internalName, "", ""); + } + + public static KeepReference memberReference(String internalName, String name, String desc) { + checkArgument(!internalName.isEmpty()); + checkArgument(!name.isEmpty()); + checkArgument(!desc.isEmpty()); + return new AutoValue_KeepReference(internalName, name, desc); + } + + public final boolean isMemberReference() { + return !name().isEmpty(); + } + + public final boolean isMethodReference() { + return desc().startsWith("("); + } + + public final boolean isFieldReference() { + return isMemberReference() && !isMethodReference(); + } + + public abstract String internalName(); + public abstract String name(); + public abstract String desc(); +} diff --git a/src/tools/android/java/com/google/devtools/build/android/desugar/scan/KeepScanner.java b/src/tools/android/java/com/google/devtools/build/android/desugar/scan/KeepScanner.java new file mode 100644 index 0000000000..5892bf5df8 --- /dev/null +++ b/src/tools/android/java/com/google/devtools/build/android/desugar/scan/KeepScanner.java @@ -0,0 +1,174 @@ +// Copyright 2018 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.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.ImmutableSet; +import com.google.devtools.build.android.Converters.ExistingPathConverter; +import com.google.devtools.build.android.Converters.PathConverter; +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 java.io.IOError; +import java.io.IOException; +import java.io.InputStream; +import java.io.PrintStream; +import java.nio.file.FileSystems; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Map; +import java.util.stream.Collectors; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.Type; + +class KeepScanner { + + public static class KeepScannerOptions extends OptionsBase { + @Option( + name = "input", + defaultValue = "null", + documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, + effectTags = OptionEffectTag.UNKNOWN, + converter = ExistingPathConverter.class, + abbrev = 'i', + help = "Input Jar with classes to scan." + ) + public Path inputJars; + + @Option( + name = "keep_file", + defaultValue = "null", + documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, + effectTags = OptionEffectTag.UNKNOWN, + converter = PathConverter.class, + help = "Where to write keep rules to." + ) + public Path keepDest; + + @Option( + name = "prefix", + defaultValue = "j$/", + documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, + effectTags = OptionEffectTag.UNKNOWN, + help = "type to scan for." + ) + public String prefix; + } + + public static void main(String... args) throws Exception { + OptionsParser parser = OptionsParser.newOptionsParser(KeepScannerOptions.class); + parser.setAllowResidue(false); + parser.enableParamsFileSupport(new ShellQuotedParamsFilePreProcessor(FileSystems.getDefault())); + parser.parseAndExitUponError(args); + + KeepScannerOptions options = parser.getOptions(KeepScannerOptions.class); + Map<String, ImmutableSet<KeepReference>> seeds = + scan(checkNotNull(options.inputJars), options.prefix); + + try (PrintStream out = + new PrintStream( + Files.newOutputStream(options.keepDest, CREATE), /*autoFlush=*/ false, "UTF-8")) { + writeKeepDirectives(out, seeds); + } + } + + /** + * Writes a -keep rule for each class listing any members to keep. We sort classes and members + * so the output is deterministic. + */ + private static void writeKeepDirectives( + PrintStream out, Map<String, ImmutableSet<KeepReference>> seeds) { + seeds + .entrySet() + .stream() + .sorted(comparing(Map.Entry::getKey)) + .forEachOrdered( + type -> { + out.printf("-keep class %s {%n", type.getKey().replace('/', '.')); + type.getValue() + .stream() + .filter(KeepReference::isMemberReference) + .sorted(comparing(KeepReference::name).thenComparing(KeepReference::desc)) + .map(ref -> toKeepDescriptor(ref)) + .distinct() // drop duplicates due to method descriptors with different returns + .forEachOrdered(line -> out.append(" ").append(line).append(";").println()); + out.printf("}%n"); + }); + } + + /** + * Scans for and returns references with owners matching the given prefix grouped by owner. + */ + private static Map<String, ImmutableSet<KeepReference>> scan(Path jarFile, String prefix) + 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())) { + return zip.stream() + .filter(entry -> entry.getName().endsWith(".class")) + .map(entry -> readFully(zip, entry)) + .parallel() + .flatMap( + content -> PrefixReferenceScanner.scan(new ClassReader(content), prefix).stream()) + .collect( + Collectors.groupingByConcurrent( + KeepReference::internalName, ImmutableSet.toImmutableSet())); + } + } + + private static byte[] readFully(ZipFile zip, ZipEntry entry) { + byte[] result = new byte[(int) entry.getSize()]; + try (InputStream content = zip.getInputStream(entry)) { + checkState(content.read(result) == result.length); + checkState(content.read() == -1); + } catch (IOException e) { + throw new IOError(e); + } + return result; + } + + private static CharSequence toKeepDescriptor(KeepReference member) { + StringBuilder result = new StringBuilder(); + if (member.isMethodReference()) { + result.append("*** ").append(member.name()).append("("); + // Ignore return type as it's unique in the source language + boolean first = true; + for (Type param : Type.getMethodType(member.desc()).getArgumentTypes()) { + if (first) { + first = false; + } else { + result.append(", "); + } + result.append(param.getClassName()); + } + result.append(")"); + } else { + checkArgument(member.isFieldReference()); + result.append("*** ").append(member.name()); // field names are unique so ignore descriptor + } + return result; + } + + private KeepScanner() {} +} diff --git a/src/tools/android/java/com/google/devtools/build/android/desugar/scan/PrefixReferenceScanner.java b/src/tools/android/java/com/google/devtools/build/android/desugar/scan/PrefixReferenceScanner.java new file mode 100644 index 0000000000..b899ccc41d --- /dev/null +++ b/src/tools/android/java/com/google/devtools/build/android/desugar/scan/PrefixReferenceScanner.java @@ -0,0 +1,405 @@ +// Copyright 2018 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.scan; + +import static com.google.common.base.Preconditions.checkArgument; + +import com.google.common.collect.ImmutableSet; +import javax.annotation.Nullable; +import org.objectweb.asm.AnnotationVisitor; +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.FieldVisitor; +import org.objectweb.asm.Handle; +import org.objectweb.asm.Label; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.Type; +import org.objectweb.asm.TypePath; + +/** {@link ClassVisitor} that records references to classes starting with a given prefix. */ +class PrefixReferenceScanner extends ClassVisitor { + + /** + * Returns references with the given prefix in the given class. + * + * @param prefix an internal name prefix, typically a package such as {@code com/google/} + */ + public static ImmutableSet<KeepReference> scan(ClassReader reader, String prefix) { + PrefixReferenceScanner scanner = new PrefixReferenceScanner(prefix); + // Frames irrelevant for Android so skip them. Don't skip debug info in case the class we're + // visiting has local variable tables (typically it doesn't anyways). + reader.accept(scanner, ClassReader.SKIP_FRAMES); + return scanner.roots.build(); + } + + private final ImmutableSet.Builder<KeepReference> roots = ImmutableSet.builder(); + private final PrefixReferenceMethodVisitor mv = new PrefixReferenceMethodVisitor(); + private final PrefixReferenceFieldVisitor fv = new PrefixReferenceFieldVisitor(); + private final PrefixReferenceAnnotationVisitor av = new PrefixReferenceAnnotationVisitor(); + + private final String prefix; + + public PrefixReferenceScanner(String prefix) { + super(Opcodes.ASM6); + this.prefix = prefix; + } + + @Override + public void visit( + int version, + int access, + String name, + String signature, + String superName, + String[] interfaces) { + checkArgument(!name.startsWith(prefix)); + if (superName != null) { + classReference(superName); + } + classReferences(interfaces); + super.visit(version, access, name, signature, superName, interfaces); + } + + @Override + public AnnotationVisitor visitAnnotation(String desc, boolean visible) { + typeReference(desc); + return av; + } + + @Override + public void visitOuterClass(String owner, String name, String desc) { + classReference(owner); + if (desc != null) { + typeReference(Type.getMethodType(desc)); + } + } + + @Override + public AnnotationVisitor visitTypeAnnotation( + int typeRef, TypePath typePath, String desc, boolean visible) { + typeReference(desc); + return av; + } + + @Override + public void visitInnerClass(String name, String outerName, String innerName, int access) { + classReference(name); + if (outerName != null) { + classReference(outerName); + } + } + + @Override + public FieldVisitor visitField( + int access, String name, String desc, String signature, Object value) { + typeReference(desc); + return fv; + } + + @Override + public MethodVisitor visitMethod( + int access, String name, String desc, String signature, String[] exceptions) { + typeReference(Type.getMethodType(desc)); + classReferences(exceptions); + return mv; + } + + private void classReferences(@Nullable String[] internalNames) { + if (internalNames != null) { + for (String itf : internalNames) { + classReference(itf); + } + } + } + + // The following methods are package-private so they don't incur bridge methods when called from + // inner classes below. + + void classReference(String internalName) { + checkArgument(internalName.charAt(0) != '[' && internalName.charAt(0) != '(', internalName); + checkArgument(!internalName.endsWith(";"), internalName); + if (internalName.startsWith(prefix)) { + roots.add(KeepReference.classReference(internalName)); + } + } + + void objectReference(String internalName) { + // don't call this for method types, convert to Type instead + checkArgument(internalName.charAt(0) != '(', internalName); + if (internalName.charAt(0) == '[') { + typeReference(internalName); + } else { + classReference(internalName); + } + } + + void typeReference(String typeDesc) { + // don't call this for method types, convert to Type instead + checkArgument(typeDesc.charAt(0) != '(', typeDesc); + + int lpos = typeDesc.lastIndexOf('[') + 1; + if (typeDesc.charAt(lpos) == 'L') { + checkArgument(typeDesc.endsWith(";"), typeDesc); + classReference(typeDesc.substring(lpos, typeDesc.length() - 1)); + } else { + // else primitive or primitive array + checkArgument(typeDesc.length() == lpos + 1, typeDesc); + switch (typeDesc.charAt(lpos)) { + case 'B': + case 'C': + case 'S': + case 'I': + case 'J': + case 'D': + case 'F': + case 'Z': + break; + default: + throw new AssertionError("Unexpected type descriptor: " + typeDesc); + } + } + } + + void typeReference(Type type) { + switch (type.getSort()) { + case Type.ARRAY: + typeReference(type.getElementType()); + break; + case Type.OBJECT: + classReference(type.getInternalName()); + break; + + case Type.METHOD: + for (Type param : type.getArgumentTypes()) { + typeReference(param); + } + typeReference(type.getReturnType()); + break; + + default: + break; + } + } + + void fieldReference(String owner, String name, String desc) { + objectReference(owner); + typeReference(desc); + if (owner.startsWith(prefix)) { + roots.add(KeepReference.memberReference(owner, name, desc)); + } + } + + void methodReference(String owner, String name, String desc) { + checkArgument(desc.charAt(0) == '(', desc); + objectReference(owner); + typeReference(Type.getMethodType(desc)); + if (owner.startsWith(prefix)) { + roots.add(KeepReference.memberReference(owner, name, desc)); + } + } + + void handleReference(Handle handle) { + switch (handle.getTag()) { + case Opcodes.H_GETFIELD: + case Opcodes.H_GETSTATIC: + case Opcodes.H_PUTFIELD: + case Opcodes.H_PUTSTATIC: + fieldReference(handle.getOwner(), handle.getName(), handle.getDesc()); + break; + + default: + methodReference(handle.getOwner(), handle.getName(), handle.getDesc()); + break; + } + } + + private class PrefixReferenceMethodVisitor extends MethodVisitor { + + public PrefixReferenceMethodVisitor() { + super(Opcodes.ASM6); + } + + @Override + public AnnotationVisitor visitAnnotationDefault() { + return av; + } + + @Override + public AnnotationVisitor visitAnnotation(String desc, boolean visible) { + typeReference(desc); + return av; + } + + @Override + public AnnotationVisitor visitTypeAnnotation( + int typeRef, TypePath typePath, String desc, boolean visible) { + typeReference(desc); + return av; + } + + @Override + public AnnotationVisitor visitParameterAnnotation(int parameter, String desc, boolean visible) { + typeReference(desc); + return av; + } + + @Override + public void visitTypeInsn(int opcode, String type) { + objectReference(type); + } + + @Override + public void visitFieldInsn(int opcode, String owner, String name, String desc) { + fieldReference(owner, name, desc); + } + + @Override + @SuppressWarnings("deprecation") // Implementing deprecated method to be sure + public void visitMethodInsn(int opcode, String owner, String name, String desc) { + visitMethodInsn(opcode, owner, name, desc, opcode == Opcodes.INVOKEINTERFACE); + } + + @Override + public void visitMethodInsn(int opcode, String owner, String name, String desc, boolean itf) { + methodReference(owner, name, desc); + } + + @Override + public void visitInvokeDynamicInsn(String name, String desc, Handle bsm, Object... bsmArgs) { + typeReference(Type.getMethodType(desc)); + handleReference(bsm); + for (Object bsmArg : bsmArgs) { + visitConstant(bsmArg); + } + } + + @Override + public void visitLdcInsn(Object cst) { + visitConstant(cst); + } + + private void visitConstant(Object cst) { + if (cst instanceof Type) { + typeReference((Type) cst); + } else if (cst instanceof Handle) { + handleReference((Handle) cst); + } else { + // Check for other expected types as javadoc recommends + checkArgument( + cst instanceof String + || cst instanceof Integer + || cst instanceof Long + || cst instanceof Float + || cst instanceof Double, + "Unexpected constant: ", cst); + } + } + + @Override + public void visitMultiANewArrayInsn(String desc, int dims) { + typeReference(desc); + } + + @Override + public AnnotationVisitor visitInsnAnnotation( + int typeRef, TypePath typePath, String desc, boolean visible) { + typeReference(desc); + return av; + } + + @Override + public void visitTryCatchBlock(Label start, Label end, Label handler, String type) { + if (type != null) { + classReference(type); + } + } + + @Override + public AnnotationVisitor visitTryCatchAnnotation( + int typeRef, TypePath typePath, String desc, boolean visible) { + typeReference(desc); + return av; + } + + @Override + public void visitLocalVariable( + String name, String desc, String signature, Label start, Label end, int index) { + typeReference(desc); + } + + @Override + public AnnotationVisitor visitLocalVariableAnnotation( + int typeRef, + TypePath typePath, + Label[] start, + Label[] end, + int[] index, + String desc, + boolean visible) { + typeReference(desc); + return av; + } + } + + private class PrefixReferenceFieldVisitor extends FieldVisitor { + + public PrefixReferenceFieldVisitor() { + super(Opcodes.ASM6); + } + + @Override + public AnnotationVisitor visitAnnotation(String desc, boolean visible) { + typeReference(desc); + return av; + } + + @Override + public AnnotationVisitor visitTypeAnnotation( + int typeRef, TypePath typePath, String desc, boolean visible) { + typeReference(desc); + return av; + } + } + + private class PrefixReferenceAnnotationVisitor extends AnnotationVisitor { + + public PrefixReferenceAnnotationVisitor() { + super(Opcodes.ASM6); + } + + @Override + public void visit(String name, Object value) { + if (value instanceof Type) { + typeReference((Type) value); + } + } + + @Override + public void visitEnum(String name, String desc, String value) { + fieldReference(desc.substring(1, desc.length() - 1), value, desc); + } + + @Override + public AnnotationVisitor visitAnnotation(String name, String desc) { + typeReference(desc); + return av; + } + + @Override + public AnnotationVisitor visitArray(String name) { + return av; + } + } +} |