diff options
8 files changed, 187 insertions, 36 deletions
diff --git a/tools/class2greylist/src/com/android/class2greylist/Class2Greylist.java b/tools/class2greylist/src/com/android/class2greylist/Class2Greylist.java index 53157a323e..3e2f6f27ea 100644 --- a/tools/class2greylist/src/com/android/class2greylist/Class2Greylist.java +++ b/tools/class2greylist/src/com/android/class2greylist/Class2Greylist.java @@ -17,6 +17,7 @@ package com.android.class2greylist; import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Joiner; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Sets; @@ -41,6 +42,7 @@ import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.function.Predicate; /** * Build time tool for extracting a list of members from jar files that have the @UsedByApps @@ -55,9 +57,10 @@ public class Class2Greylist { private final String mPublicApiListFile; private final String[] mPerSdkOutputFiles; private final String mWhitelistFile; + private final String mCsvMetadataFile; private final String[] mJarFiles; private final GreylistConsumer mOutput; - private final Set<Integer> mAllowedSdkVersions; + private final Predicate<Integer> mAllowedSdkVersions; private final Set<String> mPublicApis; @@ -95,10 +98,17 @@ public class Class2Greylist { .hasArgs(0) .create('m')); options.addOption(OptionBuilder + .withLongOpt("write-metadata-csv") + .hasArgs(1) + .withDescription("Specify a file to write API metaadata to. This is a CSV file " + + "containing any annotation properties for all members. Do not use in " + + "conjunction with --write-greylist or --write-whitelist.") + .create('c')); + options.addOption(OptionBuilder .withLongOpt("help") .hasArgs(0) .withDescription("Show this help") - .create("h")); + .create('h')); CommandLineParser parser = new GnuParser(); CommandLine cmd; @@ -132,6 +142,7 @@ public class Class2Greylist { cmd.getOptionValue('p', null), cmd.getOptionValues('g'), cmd.getOptionValue('w', null), + cmd.getOptionValue('c', null), jarFiles); c2gl.main(); } catch (IOException e) { @@ -149,22 +160,33 @@ public class Class2Greylist { @VisibleForTesting Class2Greylist(Status status, String publicApiListFile, String[] perSdkLevelOutputFiles, - String whitelistOutputFile, String[] jarFiles) throws IOException { + String whitelistOutputFile, String csvMetadataFile, String[] jarFiles) + throws IOException { mStatus = status; mPublicApiListFile = publicApiListFile; mPerSdkOutputFiles = perSdkLevelOutputFiles; mWhitelistFile = whitelistOutputFile; + mCsvMetadataFile = csvMetadataFile; mJarFiles = jarFiles; - if (mPerSdkOutputFiles != null) { + if (mCsvMetadataFile != null) { + mOutput = new CsvGreylistConsumer(mStatus, mCsvMetadataFile); + mAllowedSdkVersions = x -> true; + } else { Map<Integer, String> outputFiles = readGreylistMap(mStatus, mPerSdkOutputFiles); mOutput = new FileWritingGreylistConsumer(mStatus, outputFiles, mWhitelistFile); - mAllowedSdkVersions = outputFiles.keySet(); - } else { - // TODO remove this once per-SDK greylist support integrated into the build. - // Right now, mPerSdkOutputFiles is always null as the build never passes the - // corresponding command lind flags. Once the build is updated, can remove this. - mOutput = new SystemOutGreylistConsumer(); - mAllowedSdkVersions = new HashSet<>(Arrays.asList(null, 26, 28)); + mAllowedSdkVersions = new Predicate<Integer>(){ + @Override + public boolean test(Integer i) { + return outputFiles.keySet().contains(i); + } + + @Override + public String toString() { + // we reply on this toString behaviour for readable error messages in + // GreylistAnnotationHandler + return Joiner.on(",").join(outputFiles.keySet()); + } + }; } if (mPublicApiListFile != null) { diff --git a/tools/class2greylist/src/com/android/class2greylist/CsvGreylistConsumer.java b/tools/class2greylist/src/com/android/class2greylist/CsvGreylistConsumer.java new file mode 100644 index 0000000000..7d28b317f0 --- /dev/null +++ b/tools/class2greylist/src/com/android/class2greylist/CsvGreylistConsumer.java @@ -0,0 +1,35 @@ +package com.android.class2greylist; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.PrintStream; +import java.util.Map; + +public class CsvGreylistConsumer implements GreylistConsumer { + + private final Status mStatus; + private final CsvWriter mCsvWriter; + + public CsvGreylistConsumer(Status status, String csvMetadataFile) throws FileNotFoundException { + mStatus = status; + mCsvWriter = new CsvWriter( + new PrintStream(new FileOutputStream(new File(csvMetadataFile)))); + } + + @Override + public void greylistEntry(String signature, Integer maxTargetSdk, + Map<String, String> annotationProperties) { + annotationProperties.put("signature", signature); + mCsvWriter.addRow(annotationProperties); + } + + @Override + public void whitelistEntry(String signature) { + } + + @Override + public void close() { + mCsvWriter.close(); + } +} diff --git a/tools/class2greylist/src/com/android/class2greylist/CsvWriter.java b/tools/class2greylist/src/com/android/class2greylist/CsvWriter.java new file mode 100644 index 0000000000..3cfec30f23 --- /dev/null +++ b/tools/class2greylist/src/com/android/class2greylist/CsvWriter.java @@ -0,0 +1,49 @@ +package com.android.class2greylist; + +import com.google.common.base.Joiner; + +import java.io.PrintStream; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * Helper class for writing data to a CSV file. + * + * This class does not write anything to its output until it is closed, so it can gather a set of + * all columns before writing the header row. + */ +public class CsvWriter { + + private final PrintStream mOutput; + private final ArrayList<Map<String, String>> mContents; + private final Set<String> mColumns; + + public CsvWriter(PrintStream out) { + mOutput = out; + mContents = new ArrayList<>(); + mColumns = new HashSet<>(); + } + + public void addRow(Map<String, String> values) { + mColumns.addAll(values.keySet()); + mContents.add(values); + } + + public void close() { + List<String> columns = new ArrayList<>(mColumns); + columns.sort(Comparator.naturalOrder()); + mOutput.println(columns.stream().collect(Collectors.joining(","))); + for (Map<String, String> row : mContents) { + mOutput.println(columns.stream().map(column -> row.getOrDefault(column, "")).collect( + Collectors.joining(","))); + } + mOutput.close(); + } + + +} diff --git a/tools/class2greylist/src/com/android/class2greylist/FileWritingGreylistConsumer.java b/tools/class2greylist/src/com/android/class2greylist/FileWritingGreylistConsumer.java index bfd23102b9..b3ed1b16f9 100644 --- a/tools/class2greylist/src/com/android/class2greylist/FileWritingGreylistConsumer.java +++ b/tools/class2greylist/src/com/android/class2greylist/FileWritingGreylistConsumer.java @@ -44,7 +44,8 @@ public class FileWritingGreylistConsumer implements GreylistConsumer { } @Override - public void greylistEntry(String signature, Integer maxTargetSdk) { + public void greylistEntry( + String signature, Integer maxTargetSdk, Map<String, String> annotationProperties) { PrintStream p = mSdkToPrintStreamMap.get(maxTargetSdk); if (p == null) { mStatus.error("No output file for signature %s with maxTargetSdk of %d", signature, diff --git a/tools/class2greylist/src/com/android/class2greylist/GreylistAnnotationHandler.java b/tools/class2greylist/src/com/android/class2greylist/GreylistAnnotationHandler.java index 460f2c3c22..fed9f9210d 100644 --- a/tools/class2greylist/src/com/android/class2greylist/GreylistAnnotationHandler.java +++ b/tools/class2greylist/src/com/android/class2greylist/GreylistAnnotationHandler.java @@ -11,6 +11,8 @@ import org.apache.bcel.classfile.FieldOrMethod; import org.apache.bcel.classfile.Method; import org.apache.bcel.classfile.SimpleElementValue; +import java.util.HashMap; +import java.util.Map; import java.util.Set; import java.util.function.Predicate; @@ -36,7 +38,7 @@ public class GreylistAnnotationHandler implements AnnotationHandler { private final Status mStatus; private final Predicate<GreylistMember> mGreylistFilter; private final GreylistConsumer mGreylistConsumer; - private final Set<Integer> mValidMaxTargetSdkValues; + private final Predicate<Integer> mValidMaxTargetSdkValues; /** * Represents a member of a class file (a field or method). @@ -73,7 +75,7 @@ public class GreylistAnnotationHandler implements AnnotationHandler { Status status, GreylistConsumer greylistConsumer, Set<String> publicApis, - Set<Integer> validMaxTargetSdkValues) { + Predicate<Integer> validMaxTargetSdkValues) { this(status, greylistConsumer, member -> !(member.bridge && publicApis.contains(member.signature)), validMaxTargetSdkValues); @@ -84,7 +86,7 @@ public class GreylistAnnotationHandler implements AnnotationHandler { Status status, GreylistConsumer greylistConsumer, Predicate<GreylistMember> greylistFilter, - Set<Integer> validMaxTargetSdkValues) { + Predicate<Integer> validMaxTargetSdkValues) { mStatus = status; mGreylistConsumer = greylistConsumer; mGreylistFilter = greylistFilter; @@ -101,6 +103,7 @@ public class GreylistAnnotationHandler implements AnnotationHandler { } String signature = context.getMemberDescriptor(); Integer maxTargetSdk = null; + Map<String, String> allValues = new HashMap<String, String>(); for (ElementValuePair property : annotation.getElementValuePairs()) { switch (property.getNameString()) { case EXPECTED_SIGNATURE: @@ -110,9 +113,10 @@ public class GreylistAnnotationHandler implements AnnotationHandler { maxTargetSdk = verifyAndGetMaxTargetSdk(context, property); break; } + allValues.put(property.getNameString(), property.getValue().stringifyValue()); } if (mGreylistFilter.test(new GreylistMember(signature, bridge, maxTargetSdk))) { - mGreylistConsumer.greylistEntry(signature, maxTargetSdk); + mGreylistConsumer.greylistEntry(signature, maxTargetSdk, allValues); } } @@ -131,13 +135,14 @@ public class GreylistAnnotationHandler implements AnnotationHandler { if (property.getValue().getElementValueType() != ElementValue.PRIMITIVE_INT) { context.reportError("Expected property %s to be of type int; got %d", property.getNameString(), property.getValue().getElementValueType()); + return null; } int value = ((SimpleElementValue) property.getValue()).getValueInt(); - if (!mValidMaxTargetSdkValues.contains(value)) { + if (!mValidMaxTargetSdkValues.test(value)) { context.reportError("Invalid value for %s: got %d, expected one of [%s]", property.getNameString(), value, - Joiner.on(",").join(mValidMaxTargetSdkValues)); + mValidMaxTargetSdkValues); return null; } return value; diff --git a/tools/class2greylist/src/com/android/class2greylist/GreylistConsumer.java b/tools/class2greylist/src/com/android/class2greylist/GreylistConsumer.java index fd855e88ed..afded37e66 100644 --- a/tools/class2greylist/src/com/android/class2greylist/GreylistConsumer.java +++ b/tools/class2greylist/src/com/android/class2greylist/GreylistConsumer.java @@ -1,5 +1,7 @@ package com.android.class2greylist; +import java.util.Map; + public interface GreylistConsumer { /** * Handle a new greylist entry. @@ -7,7 +9,8 @@ public interface GreylistConsumer { * @param signature Signature of the member. * @param maxTargetSdk maxTargetSdk value from the annotation, or null if none set. */ - void greylistEntry(String signature, Integer maxTargetSdk); + void greylistEntry( + String signature, Integer maxTargetSdk, Map<String, String> annotationProperties); /** * Handle a new whitelist entry. diff --git a/tools/class2greylist/src/com/android/class2greylist/SystemOutGreylistConsumer.java b/tools/class2greylist/src/com/android/class2greylist/SystemOutGreylistConsumer.java index ad5ad705b4..f86ac6ec68 100644 --- a/tools/class2greylist/src/com/android/class2greylist/SystemOutGreylistConsumer.java +++ b/tools/class2greylist/src/com/android/class2greylist/SystemOutGreylistConsumer.java @@ -1,8 +1,11 @@ package com.android.class2greylist; +import java.util.Map; + public class SystemOutGreylistConsumer implements GreylistConsumer { @Override - public void greylistEntry(String signature, Integer maxTargetSdk) { + public void greylistEntry( + String signature, Integer maxTargetSdk, Map<String, String> annotationValues) { System.out.println(signature); } diff --git a/tools/class2greylist/test/src/com/android/class2greylist/GreylistAnnotationHandlerTest.java b/tools/class2greylist/test/src/com/android/class2greylist/GreylistAnnotationHandlerTest.java index 1a4bfb8283..edf2ecd84d 100644 --- a/tools/class2greylist/test/src/com/android/class2greylist/GreylistAnnotationHandlerTest.java +++ b/tools/class2greylist/test/src/com/android/class2greylist/GreylistAnnotationHandlerTest.java @@ -60,7 +60,7 @@ public class GreylistAnnotationHandlerTest extends AnnotationHandlerTestBase { Predicate<GreylistAnnotationHandler.GreylistMember> greylistFilter, Set<Integer> validMaxTargetSdkValues) { return new GreylistAnnotationHandler( - mStatus, mConsumer, greylistFilter, validMaxTargetSdkValues); + mStatus, mConsumer, greylistFilter, x -> validMaxTargetSdkValues.contains(x)); } @Test @@ -80,7 +80,7 @@ public class GreylistAnnotationHandlerTest extends AnnotationHandlerTestBase { assertNoErrors(); ArgumentCaptor<String> greylist = ArgumentCaptor.forClass(String.class); - verify(mConsumer, times(1)).greylistEntry(greylist.capture(), any()); + verify(mConsumer, times(1)).greylistEntry(greylist.capture(), any(), any()); assertThat(greylist.getValue()).isEqualTo("La/b/Class;->method()V"); } @@ -101,7 +101,7 @@ public class GreylistAnnotationHandlerTest extends AnnotationHandlerTestBase { assertNoErrors(); ArgumentCaptor<String> greylist = ArgumentCaptor.forClass(String.class); - verify(mConsumer, times(1)).greylistEntry(greylist.capture(), any()); + verify(mConsumer, times(1)).greylistEntry(greylist.capture(), any(), any()); assertThat(greylist.getValue()).isEqualTo("La/b/Class;-><init>()V"); } @@ -122,7 +122,7 @@ public class GreylistAnnotationHandlerTest extends AnnotationHandlerTestBase { assertNoErrors(); ArgumentCaptor<String> greylist = ArgumentCaptor.forClass(String.class); - verify(mConsumer, times(1)).greylistEntry(greylist.capture(), any()); + verify(mConsumer, times(1)).greylistEntry(greylist.capture(), any(), any()); assertThat(greylist.getValue()).isEqualTo("La/b/Class;->i:I"); } @@ -143,7 +143,7 @@ public class GreylistAnnotationHandlerTest extends AnnotationHandlerTestBase { assertNoErrors(); ArgumentCaptor<String> greylist = ArgumentCaptor.forClass(String.class); - verify(mConsumer, times(1)).greylistEntry(greylist.capture(), any()); + verify(mConsumer, times(1)).greylistEntry(greylist.capture(), any(), any()); assertThat(greylist.getValue()).isEqualTo("La/b/Class;->method()V"); } @@ -184,7 +184,7 @@ public class GreylistAnnotationHandlerTest extends AnnotationHandlerTestBase { assertNoErrors(); ArgumentCaptor<String> greylist = ArgumentCaptor.forClass(String.class); - verify(mConsumer, times(1)).greylistEntry(greylist.capture(), any()); + verify(mConsumer, times(1)).greylistEntry(greylist.capture(), any(), any()); assertThat(greylist.getValue()).isEqualTo("La/b/Class$Inner;->method()V"); } @@ -202,7 +202,7 @@ public class GreylistAnnotationHandlerTest extends AnnotationHandlerTestBase { ).visit(); assertNoErrors(); - verify(mConsumer, never()).greylistEntry(any(String.class), any()); + verify(mConsumer, never()).greylistEntry(any(String.class), any(), any()); } @Test @@ -222,7 +222,7 @@ public class GreylistAnnotationHandlerTest extends AnnotationHandlerTestBase { assertNoErrors(); ArgumentCaptor<String> greylist = ArgumentCaptor.forClass(String.class); - verify(mConsumer, times(1)).greylistEntry(greylist.capture(), any()); + verify(mConsumer, times(1)).greylistEntry(greylist.capture(), any(), any()); assertThat(greylist.getValue()).isEqualTo("La/b/Class;->method(Ljava/lang/String;)V"); } @@ -252,7 +252,7 @@ public class GreylistAnnotationHandlerTest extends AnnotationHandlerTestBase { assertNoErrors(); ArgumentCaptor<String> greylist = ArgumentCaptor.forClass(String.class); // A bridge method is generated for the above, so we expect 2 greylist entries. - verify(mConsumer, times(2)).greylistEntry(greylist.capture(), any()); + verify(mConsumer, times(2)).greylistEntry(greylist.capture(), any(), any()); assertThat(greylist.getAllValues()).containsExactly( "La/b/Class;->method(Ljava/lang/Object;)V", "La/b/Class;->method(Ljava/lang/String;)V"); @@ -284,7 +284,7 @@ public class GreylistAnnotationHandlerTest extends AnnotationHandlerTestBase { assertNoErrors(); ArgumentCaptor<String> greylist = ArgumentCaptor.forClass(String.class); // A bridge method is generated for the above, so we expect 2 greylist entries. - verify(mConsumer, times(2)).greylistEntry(greylist.capture(), any()); + verify(mConsumer, times(2)).greylistEntry(greylist.capture(), any(), any()); assertThat(greylist.getAllValues()).containsExactly( "La/b/Class;->method(Ljava/lang/Object;)V", "La/b/Class;->method(Ljava/lang/String;)V"); @@ -322,7 +322,7 @@ public class GreylistAnnotationHandlerTest extends AnnotationHandlerTestBase { assertNoErrors(); ArgumentCaptor<String> greylist = ArgumentCaptor.forClass(String.class); // A bridge method is generated for the above, so we expect 2 greylist entries. - verify(mConsumer, times(2)).greylistEntry(greylist.capture(), any()); + verify(mConsumer, times(2)).greylistEntry(greylist.capture(), any(), any()); assertThat(greylist.getAllValues()).containsExactly( "La/b/Class;->method(Ljava/lang/Object;)V", "La/b/Base;->method(Ljava/lang/Object;)V"); @@ -355,14 +355,14 @@ public class GreylistAnnotationHandlerTest extends AnnotationHandlerTestBase { mStatus, mConsumer, publicApis, - emptySet())); + x -> false)); new AnnotationVisitor(mJavac.getCompiledClass("a.b.Base"), mStatus, handlerMap).visit(); new AnnotationVisitor(mJavac.getCompiledClass("a.b.Class"), mStatus, handlerMap).visit(); assertNoErrors(); ArgumentCaptor<String> greylist = ArgumentCaptor.forClass(String.class); // The bridge method generated for the above, is a public API so should be excluded - verify(mConsumer, times(1)).greylistEntry(greylist.capture(), any()); + verify(mConsumer, times(1)).greylistEntry(greylist.capture(), any(), any()); assertThat(greylist.getValue()).isEqualTo("La/b/Class;->method(Ljava/lang/String;)V"); } @@ -384,7 +384,7 @@ public class GreylistAnnotationHandlerTest extends AnnotationHandlerTestBase { new AnnotationVisitor(mJavac.getCompiledClass("a.b.Class"), mStatus, handlerMap).visit(); assertNoErrors(); ArgumentCaptor<String> greylist = ArgumentCaptor.forClass(String.class); - verify(mConsumer, times(1)).greylistEntry(greylist.capture(), any()); + verify(mConsumer, times(1)).greylistEntry(greylist.capture(), any(), any()); assertThat(greylist.getValue()).isEqualTo("La/b/Class;->field:I"); } @@ -423,7 +423,7 @@ public class GreylistAnnotationHandlerTest extends AnnotationHandlerTestBase { new AnnotationVisitor(mJavac.getCompiledClass("a.b.Class"), mStatus, handlerMap).visit(); assertNoErrors(); ArgumentCaptor<Integer> maxTargetSdk = ArgumentCaptor.forClass(Integer.class); - verify(mConsumer, times(1)).greylistEntry(any(), maxTargetSdk.capture()); + verify(mConsumer, times(1)).greylistEntry(any(), maxTargetSdk.capture(), any()); assertThat(maxTargetSdk.getValue()).isEqualTo(1); } @@ -445,7 +445,7 @@ public class GreylistAnnotationHandlerTest extends AnnotationHandlerTestBase { new AnnotationVisitor(mJavac.getCompiledClass("a.b.Class"), mStatus, handlerMap).visit(); assertNoErrors(); ArgumentCaptor<Integer> maxTargetSdk = ArgumentCaptor.forClass(Integer.class); - verify(mConsumer, times(1)).greylistEntry(any(), maxTargetSdk.capture()); + verify(mConsumer, times(1)).greylistEntry(any(), maxTargetSdk.capture(), any()); assertThat(maxTargetSdk.getValue()).isEqualTo(null); } @@ -468,4 +468,37 @@ public class GreylistAnnotationHandlerTest extends AnnotationHandlerTestBase { verify(mStatus, times(1)).error(any(), any()); } + @Test + public void testAnnotationPropertiesIntoMap() throws IOException { + mJavac.addSource("annotation.Anno2", Joiner.on('\n').join( + "package annotation;", + "import static java.lang.annotation.RetentionPolicy.CLASS;", + "import java.lang.annotation.Retention;", + "@Retention(CLASS)", + "public @interface Anno2 {", + " String expectedSignature() default \"\";", + " int maxTargetSdk() default Integer.MAX_VALUE;", + " long trackingBug() default 0;", + "}")); + mJavac.addSource("a.b.Class", Joiner.on('\n').join( + "package a.b;", + "import annotation.Anno2;", + "public class Class {", + " @Anno2(maxTargetSdk=2, trackingBug=123456789)", + " public int field;", + "}")); + assertThat(mJavac.compile()).isTrue(); + new AnnotationVisitor(mJavac.getCompiledClass("a.b.Class"), mStatus, + ImmutableMap.of("Lannotation/Anno2;", createGreylistHandler(x -> true, + ImmutableSet.of(2))) + ).visit(); + + assertNoErrors(); + ArgumentCaptor<Map<String, String>> properties = ArgumentCaptor.forClass(Map.class); + verify(mConsumer, times(1)).greylistEntry(any(), any(), properties.capture()); + assertThat(properties.getValue()).containsExactly( + "maxTargetSdk", "2", + "trackingBug", "123456789"); + } + } |