diff options
Diffstat (limited to 'tools')
5 files changed, 428 insertions, 0 deletions
diff --git a/tools/class2greylist/src/com/android/class2greylist/Class2Greylist.java b/tools/class2greylist/src/com/android/class2greylist/Class2Greylist.java index c0a3160a88..694a5f4a3f 100644 --- a/tools/class2greylist/src/com/android/class2greylist/Class2Greylist.java +++ b/tools/class2greylist/src/com/android/class2greylist/Class2Greylist.java @@ -166,6 +166,10 @@ public class Class2Greylist { .put(GreylistAnnotationHandler.ANNOTATION_NAME, new GreylistAnnotationHandler( mStatus, mOutput, mPublicApis, mAllowedSdkVersions)) + .put(CovariantReturnTypeHandler.ANNOTATION_NAME, + new CovariantReturnTypeHandler(mOutput, mPublicApis)) + .put(CovariantReturnTypeMultiHandler.ANNOTATION_NAME, + new CovariantReturnTypeMultiHandler(mOutput, mPublicApis)) .build(); } diff --git a/tools/class2greylist/src/com/android/class2greylist/CovariantReturnTypeHandler.java b/tools/class2greylist/src/com/android/class2greylist/CovariantReturnTypeHandler.java new file mode 100644 index 0000000000..afd15b4c59 --- /dev/null +++ b/tools/class2greylist/src/com/android/class2greylist/CovariantReturnTypeHandler.java @@ -0,0 +1,89 @@ +package com.android.class2greylist; + +import com.google.common.base.Preconditions; + +import org.apache.bcel.classfile.AnnotationEntry; +import org.apache.bcel.classfile.ElementValuePair; +import org.apache.bcel.classfile.Method; + +import java.util.Locale; +import java.util.Set; + +/** + * Handles {@code CovariantReturnType} annotations, generating whitelist + * entries from them. + * + * <p>A whitelist entry is generated with the same descriptor as the original + * method, but with the return type replaced with than specified by the + * {@link #RETURN_TYPE} property. + * + * <p>Methods are also validated against the public API list, to assert that + * the annotated method is already a public API. + */ +public class CovariantReturnTypeHandler implements AnnotationHandler { + + private static final String SHORT_NAME = "CovariantReturnType"; + public static final String ANNOTATION_NAME = "Ldalvik/annotation/codegen/CovariantReturnType;"; + + private static final String RETURN_TYPE = "returnType"; + + private final GreylistConsumer mConsumer; + private final Set<String> mPublicApis; + + public CovariantReturnTypeHandler(GreylistConsumer consumer, Set<String> publicApis) { + mConsumer = consumer; + mPublicApis = publicApis; + } + + @Override + public void handleAnnotation(AnnotationEntry annotation, AnnotationContext context) { + // Verify that the annotation has been applied to what we expect, and + // has the right form. Note, this should not strictly be necessary, as + // the annotation has a target of just 'method' and the property + // returnType does not have a default value, but checking makes the code + // less brittle to future changes. + if (!(context.member instanceof Method)) { + context.reportError("Cannot specify %s on a field", RETURN_TYPE); + return; + } + String returnType = findReturnType(annotation); + if (returnType == null) { + context.reportError("No %s set on @%s", RETURN_TYPE, SHORT_NAME); + return; + } + if (!mPublicApis.contains(context.getMemberDescriptor())) { + context.reportError("Found @%s on non-SDK method", SHORT_NAME); + return; + } + + // Generate the signature of overload that we expect the annotation will + // cause the platform dexer to create. + String typeSignature = context.member.getSignature(); + int closingBrace = typeSignature.indexOf(')'); + Preconditions.checkState(closingBrace != -1, + "No ) found in method type signature %s", typeSignature); + typeSignature = new StringBuilder() + .append(typeSignature.substring(0, closingBrace + 1)) + .append(returnType) + .toString(); + String signature = String.format(Locale.US, context.signatureFormatString, + context.getClassDescriptor(), context.member.getName(), typeSignature); + + if (mPublicApis.contains(signature)) { + context.reportError("Signature %s generated from @%s already exists as a public API", + signature, SHORT_NAME); + return; + } + mConsumer.whitelistEntry(signature); + } + + private String findReturnType(AnnotationEntry a) { + for (ElementValuePair property : a.getElementValuePairs()) { + if (property.getNameString().equals(RETURN_TYPE)) { + return property.getValue().stringifyValue(); + } + } + // not found + return null; + } +} diff --git a/tools/class2greylist/src/com/android/class2greylist/CovariantReturnTypeMultiHandler.java b/tools/class2greylist/src/com/android/class2greylist/CovariantReturnTypeMultiHandler.java new file mode 100644 index 0000000000..bd0bf79169 --- /dev/null +++ b/tools/class2greylist/src/com/android/class2greylist/CovariantReturnTypeMultiHandler.java @@ -0,0 +1,72 @@ +package com.android.class2greylist; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Preconditions; + +import org.apache.bcel.classfile.AnnotationElementValue; +import org.apache.bcel.classfile.AnnotationEntry; +import org.apache.bcel.classfile.ArrayElementValue; +import org.apache.bcel.classfile.ElementValue; +import org.apache.bcel.classfile.ElementValuePair; + +import java.util.Set; + +/** + * Handles {@code CovariantReturnType$CovariantReturnTypes} annotations, which + * are generated by the compiler when multiple {@code CovariantReturnType} + * annotations appear on a single method. + * + * <p>The enclosed annotations are passed to {@link CovariantReturnTypeHandler}. + */ +public class CovariantReturnTypeMultiHandler implements AnnotationHandler { + + public static final String ANNOTATION_NAME = + "Ldalvik/annotation/codegen/CovariantReturnType$CovariantReturnTypes;"; + + private static final String VALUE = "value"; + + private final CovariantReturnTypeHandler mWrappedHandler; + private final String mInnerAnnotationName; + + public CovariantReturnTypeMultiHandler(GreylistConsumer consumer, Set<String> publicApis) { + this(consumer, publicApis, CovariantReturnTypeHandler.ANNOTATION_NAME); + } + + @VisibleForTesting + public CovariantReturnTypeMultiHandler(GreylistConsumer consumer, Set<String> publicApis, + String innerAnnotationName) { + mWrappedHandler = new CovariantReturnTypeHandler(consumer, publicApis); + mInnerAnnotationName = innerAnnotationName; + } + + @Override + public void handleAnnotation(AnnotationEntry annotation, AnnotationContext context) { + // Verify that the annotation has the form we expect + ElementValuePair value = findValue(annotation); + if (value == null) { + context.reportError("No value found on CovariantReturnType$CovariantReturnTypes"); + return; + } + Preconditions.checkArgument(value.getValue() instanceof ArrayElementValue); + ArrayElementValue array = (ArrayElementValue) value.getValue(); + + // call wrapped handler on each enclosed annotation: + for (ElementValue v : array.getElementValuesArray()) { + Preconditions.checkArgument(v instanceof AnnotationElementValue); + AnnotationElementValue aev = (AnnotationElementValue) v; + Preconditions.checkArgument( + aev.getAnnotationEntry().getAnnotationType().equals(mInnerAnnotationName)); + mWrappedHandler.handleAnnotation(aev.getAnnotationEntry(), context); + } + } + + private ElementValuePair findValue(AnnotationEntry a) { + for (ElementValuePair property : a.getElementValuePairs()) { + if (property.getNameString().equals(VALUE)) { + return property; + } + } + // not found + return null; + } +} diff --git a/tools/class2greylist/test/src/com/android/class2greylist/CovariantReturnTypeHandlerTest.java b/tools/class2greylist/test/src/com/android/class2greylist/CovariantReturnTypeHandlerTest.java new file mode 100644 index 0000000000..10fae9b11c --- /dev/null +++ b/tools/class2greylist/test/src/com/android/class2greylist/CovariantReturnTypeHandlerTest.java @@ -0,0 +1,147 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * 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.android.class2greylist; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import static java.util.Collections.emptySet; + +import com.google.common.base.Joiner; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.ArgumentCaptor; + +import java.io.IOException; +import java.util.Map; + +public class CovariantReturnTypeHandlerTest extends AnnotationHandlerTestBase { + + private static final String ANNOTATION = "Lannotation/Annotation;"; + + @Before + public void setup() throws IOException { + // To keep the test simpler and more concise, we don't use the real + // @CovariantReturnType annotation here, but use our own @Annotation. + // It doesn't have to match the real annotation, just have the same + // property (returnType). + mJavac.addSource("annotation.Annotation", Joiner.on('\n').join( + "package annotation;", + "import static java.lang.annotation.RetentionPolicy.CLASS;", + "import java.lang.annotation.Retention;", + "@Retention(CLASS)", + "public @interface Annotation {", + " Class<?> returnType();", + "}")); + } + + @Test + public void testReturnTypeWhitelisted() throws IOException { + mJavac.addSource("a.b.Class", Joiner.on('\n').join( + "package a.b;", + "import annotation.Annotation;", + "public class Class {", + " @Annotation(returnType=Integer.class)", + " public String method() {return null;}", + "}")); + assertThat(mJavac.compile()).isTrue(); + + Map<String, AnnotationHandler> handlerMap = + ImmutableMap.of(ANNOTATION, + new CovariantReturnTypeHandler( + mConsumer, + ImmutableSet.of("La/b/Class;->method()Ljava/lang/String;"))); + new AnnotationVisitor(mJavac.getCompiledClass("a.b.Class"), mStatus, handlerMap).visit(); + + assertNoErrors(); + verify(mConsumer, times(1)).whitelistEntry(eq("La/b/Class;->method()Ljava/lang/Integer;")); + } + + @Test + public void testAnnotatedMemberNotPublicApi() throws IOException { + mJavac.addSource("a.b.Class", Joiner.on('\n').join( + "package a.b;", + "import annotation.Annotation;", + "public class Class {", + " @Annotation(returnType=Integer.class)", + " public String method() {return null;}", + "}")); + assertThat(mJavac.compile()).isTrue(); + + Map<String, AnnotationHandler> handlerMap = + ImmutableMap.of(ANNOTATION, + new CovariantReturnTypeHandler( + mConsumer, + emptySet())); + new AnnotationVisitor(mJavac.getCompiledClass("a.b.Class"), mStatus, handlerMap).visit(); + + verify(mStatus, atLeastOnce()).error(any(), any()); + } + + @Test + public void testReturnTypeAlreadyWhitelisted() throws IOException { + mJavac.addSource("a.b.Class", Joiner.on('\n').join( + "package a.b;", + "import annotation.Annotation;", + "public class Class {", + " @Annotation(returnType=Integer.class)", + " public String method() {return null;}", + "}")); + assertThat(mJavac.compile()).isTrue(); + + Map<String, AnnotationHandler> handlerMap = + ImmutableMap.of(ANNOTATION, + new CovariantReturnTypeHandler( + mConsumer, + ImmutableSet.of( + "La/b/Class;->method()Ljava/lang/String;", + "La/b/Class;->method()Ljava/lang/Integer;" + ))); + new AnnotationVisitor(mJavac.getCompiledClass("a.b.Class"), mStatus, handlerMap).visit(); + + verify(mStatus, atLeastOnce()).error(any(), any()); + } + + @Test + public void testAnnotationOnField() throws IOException { + mJavac.addSource("a.b.Class", Joiner.on('\n').join( + "package a.b;", + "import annotation.Annotation;", + "public class Class {", + " @Annotation(returnType=Integer.class)", + " public String field;", + "}")); + assertThat(mJavac.compile()).isTrue(); + + Map<String, AnnotationHandler> handlerMap = + ImmutableMap.of(ANNOTATION, + new CovariantReturnTypeHandler( + mConsumer, + emptySet())); + new AnnotationVisitor(mJavac.getCompiledClass("a.b.Class"), mStatus, handlerMap).visit(); + + verify(mStatus, atLeastOnce()).error(any(), any()); + } +} diff --git a/tools/class2greylist/test/src/com/android/class2greylist/CovariantReturnTypeMultiHandlerTest.java b/tools/class2greylist/test/src/com/android/class2greylist/CovariantReturnTypeMultiHandlerTest.java new file mode 100644 index 0000000000..7f4ce62002 --- /dev/null +++ b/tools/class2greylist/test/src/com/android/class2greylist/CovariantReturnTypeMultiHandlerTest.java @@ -0,0 +1,116 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * 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.android.class2greylist; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import static java.util.Collections.emptySet; + +import com.google.common.base.Joiner; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.ArgumentCaptor; + +import java.io.IOException; +import java.util.Map; + +public class CovariantReturnTypeMultiHandlerTest extends AnnotationHandlerTestBase { + + + @Before + public void setup() throws IOException { + // To keep the test simpler and more concise, we don't use the real + // @CovariantReturnType annotation here, but use our own @Annotation + // and @Annotation.Multi that have the same semantics. It doesn't have + // to match the real annotation, just have the same properties + // (returnType and value). + mJavac.addSource("annotation.Annotation", Joiner.on('\n').join( + "package annotation;", + "import static java.lang.annotation.RetentionPolicy.CLASS;", + "import java.lang.annotation.Repeatable;", + "import java.lang.annotation.Retention;", + "@Repeatable(Annotation.Multi.class)", + "@Retention(CLASS)", + "public @interface Annotation {", + " Class<?> returnType();", + " @Retention(CLASS)", + " @interface Multi {", + " Annotation[] value();", + " }", + "}")); + } + + @Test + public void testReturnTypeMulti() throws IOException { + mJavac.addSource("a.b.Class", Joiner.on('\n').join( + "package a.b;", + "import annotation.Annotation;", + "public class Class {", + " @Annotation(returnType=Integer.class)", + " @Annotation(returnType=Long.class)", + " public String method() {return null;}", + "}")); + assertThat(mJavac.compile()).isTrue(); + + Map<String, AnnotationHandler> handlerMap = + ImmutableMap.of("Lannotation/Annotation$Multi;", + new CovariantReturnTypeMultiHandler( + mConsumer, + ImmutableSet.of("La/b/Class;->method()Ljava/lang/String;"), + "Lannotation/Annotation;")); + new AnnotationVisitor(mJavac.getCompiledClass("a.b.Class"), mStatus, handlerMap).visit(); + + assertNoErrors(); + ArgumentCaptor<String> whitelist = ArgumentCaptor.forClass(String.class); + verify(mConsumer, times(2)).whitelistEntry(whitelist.capture()); + assertThat(whitelist.getAllValues()).containsExactly( + "La/b/Class;->method()Ljava/lang/Integer;", + "La/b/Class;->method()Ljava/lang/Long;" + ); + } + + @Test + public void testReturnTypeMultiNotPublicApi() throws IOException { + mJavac.addSource("a.b.Class", Joiner.on('\n').join( + "package a.b;", + "import annotation.Annotation;", + "public class Class {", + " @Annotation(returnType=Integer.class)", + " @Annotation(returnType=Long.class)", + " public String method() {return null;}", + "}")); + assertThat(mJavac.compile()).isTrue(); + + Map<String, AnnotationHandler> handlerMap = + ImmutableMap.of("Lannotation/Annotation$Multi;", + new CovariantReturnTypeMultiHandler( + mConsumer, + emptySet(), + "Lannotation/Annotation;")); + new AnnotationVisitor(mJavac.getCompiledClass("a.b.Class"), mStatus, handlerMap).visit(); + + verify(mStatus, atLeastOnce()).error(any(), any()); + } +} |