• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 package org.robolectric.internal.bytecode;
2 
3 import com.google.common.collect.ImmutableList;
4 import com.google.common.collect.ImmutableMap;
5 import com.google.common.collect.ImmutableSet;
6 import com.google.common.collect.Sets;
7 import java.util.ArrayList;
8 import java.util.Collection;
9 import java.util.Collections;
10 import java.util.HashMap;
11 import java.util.HashSet;
12 import java.util.Iterator;
13 import java.util.List;
14 import java.util.Map;
15 import java.util.Set;
16 import java.util.stream.Collectors;
17 import org.objectweb.asm.tree.MethodInsnNode;
18 import org.robolectric.annotation.internal.DoNotInstrument;
19 import org.robolectric.annotation.internal.Instrument;
20 import org.robolectric.shadow.api.Shadow;
21 
22 /**
23  * Configuration rules for {@link SandboxClassLoader}.
24  */
25 public class InstrumentationConfiguration {
26 
newBuilder()27   public static Builder newBuilder() {
28     return new Builder();
29   }
30 
31   static final Set<String> CLASSES_TO_ALWAYS_ACQUIRE = Sets.newHashSet(
32       RobolectricInternals.class.getName(),
33       InvokeDynamicSupport.class.getName(),
34       Shadow.class.getName(),
35 
36       // these classes are deprecated and will be removed soon:
37       "org.robolectric.util.FragmentTestUtil",
38       "org.robolectric.util.FragmentTestUtil$FragmentUtilActivity"
39   );
40 
41   // Must always acquire these as they change from API level to API level
42   static final ImmutableSet<String> RESOURCES_TO_ALWAYS_ACQUIRE =
43       ImmutableSet.of("build.prop", "usr/share/zoneinfo/tzdata");
44 
45   private final List<String> instrumentedPackages;
46   private final Set<String> instrumentedClasses;
47   private final Set<String> classesToNotInstrument;
48   private final String classesToNotInstrumentRegex;
49   private final Map<String, String> classNameTranslations;
50   private final Set<MethodRef> interceptedMethods;
51   private final Set<String> classesToNotAcquire;
52   private final Set<String> packagesToNotAcquire;
53   private final Set<String> packagesToNotInstrument;
54   private int cachedHashCode;
55 
56   private final TypeMapper typeMapper;
57   private final Set<MethodRef> methodsToIntercept;
58 
InstrumentationConfiguration( Map<String, String> classNameTranslations, Collection<MethodRef> interceptedMethods, Collection<String> instrumentedPackages, Collection<String> instrumentedClasses, Collection<String> classesToNotAcquire, Collection<String> packagesToNotAquire, Collection<String> classesToNotInstrument, Collection<String> packagesToNotInstrument, String classesToNotInstrumentRegex)59   protected InstrumentationConfiguration(
60       Map<String, String> classNameTranslations,
61       Collection<MethodRef> interceptedMethods,
62       Collection<String> instrumentedPackages,
63       Collection<String> instrumentedClasses,
64       Collection<String> classesToNotAcquire,
65       Collection<String> packagesToNotAquire,
66       Collection<String> classesToNotInstrument,
67       Collection<String> packagesToNotInstrument,
68       String classesToNotInstrumentRegex) {
69     this.classNameTranslations = ImmutableMap.copyOf(classNameTranslations);
70     this.interceptedMethods = ImmutableSet.copyOf(interceptedMethods);
71     this.instrumentedPackages = ImmutableList.copyOf(instrumentedPackages);
72     this.instrumentedClasses = ImmutableSet.copyOf(instrumentedClasses);
73     this.classesToNotAcquire = ImmutableSet.copyOf(classesToNotAcquire);
74     this.packagesToNotAcquire = ImmutableSet.copyOf(packagesToNotAquire);
75     this.classesToNotInstrument = ImmutableSet.copyOf(classesToNotInstrument);
76     this.packagesToNotInstrument = ImmutableSet.copyOf(packagesToNotInstrument);
77     this.classesToNotInstrumentRegex = classesToNotInstrumentRegex;
78     this.cachedHashCode = 0;
79 
80     this.typeMapper = new TypeMapper(classNameTranslations());
81     this.methodsToIntercept = ImmutableSet.copyOf(convertToSlashes(methodsToIntercept()));
82   }
83 
84   /**
85    * Determine if {@link SandboxClassLoader} should instrument a given class.
86    *
87    * @param classDetails The class to check.
88    * @return True if the class should be instrumented.
89    */
shouldInstrument(ClassDetails classDetails)90   public boolean shouldInstrument(ClassDetails classDetails) {
91     return !classDetails.isAnnotation()
92         && !classesToNotInstrument.contains(classDetails.getName())
93         && !isInPackagesToNotInstrument(classDetails.getName())
94         && !classMatchesExclusionRegex(classDetails.getName())
95         && !classDetails.isInstrumented()
96         && !classDetails.hasAnnotation(DoNotInstrument.class)
97         && (isInInstrumentedPackage(classDetails.getName())
98             || instrumentedClasses.contains(classDetails.getName())
99             || classDetails.hasAnnotation(Instrument.class))
100         && !classDetails.hasAnnotation(
101             "org.junit.runner.RunWith"); // Don't instrument test classes.
102   }
103 
classMatchesExclusionRegex(String className)104   private boolean classMatchesExclusionRegex(String className) {
105     return classesToNotInstrumentRegex != null && className.matches(classesToNotInstrumentRegex);
106   }
107 
108   /**
109    * Determine if {@link SandboxClassLoader} should load a given class.
110    *
111    * @param   name The fully-qualified class name.
112    * @return  True if the class should be loaded.
113    */
shouldAcquire(String name)114   public boolean shouldAcquire(String name) {
115     if (CLASSES_TO_ALWAYS_ACQUIRE.contains(name)) {
116       return true;
117     }
118 
119     if (name.equals("java.util.jar.StrictJarFile")) {
120       return true;
121     }
122 
123     // android.R and com.android.internal.R classes must be loaded from the framework jar
124     if (name.matches("(android|com\\.android\\.internal)\\.R(\\$.+)?")) {
125       return true;
126     }
127 
128     // Hack. Fixes https://github.com/robolectric/robolectric/issues/1864
129     if (name.equals("javax.net.ssl.DistinguishedNameParser")
130         || name.equals("javax.microedition.khronos.opengles.GL")) {
131       return true;
132     }
133 
134     for (String packageName : packagesToNotAcquire) {
135       if (name.startsWith(packageName)) return false;
136     }
137 
138     // R classes must be loaded from system CP
139     boolean isRClass = name.matches(".*\\.R(|\\$[a-z]+)$");
140     return !isRClass && !classesToNotAcquire.contains(name);
141   }
142 
143   /**
144    * Determine if {@link SandboxClassLoader} should load a given resource.
145    *
146    * @param name The fully-qualified resource name.
147    * @return True if the resource should be loaded.
148    */
shouldAcquireResource(String name)149   public boolean shouldAcquireResource(String name) {
150     return RESOURCES_TO_ALWAYS_ACQUIRE.contains(name);
151   }
152 
methodsToIntercept()153   public Set<MethodRef> methodsToIntercept() {
154     return Collections.unmodifiableSet(interceptedMethods);
155   }
156 
157   /**
158    * Map from a requested class to an alternate stand-in, or not.
159    *
160    * @return Mapping of class name translations.
161    */
classNameTranslations()162   public Map<String, String> classNameTranslations() {
163     return Collections.unmodifiableMap(classNameTranslations);
164   }
165 
isInInstrumentedPackage(String className)166   private boolean isInInstrumentedPackage(String className) {
167     for (String instrumentedPackage : instrumentedPackages) {
168       if (className.startsWith(instrumentedPackage)) {
169         return true;
170       }
171     }
172     return false;
173   }
174 
isInPackagesToNotInstrument(String className)175   private boolean isInPackagesToNotInstrument(String className) {
176     for (String notInstrumentedPackage : packagesToNotInstrument) {
177       if (className.startsWith(notInstrumentedPackage)) {
178         return true;
179       }
180     }
181     return false;
182   }
183 
184   @Override
equals(Object o)185   public boolean equals(Object o) {
186     if (this == o) return true;
187     if (!(o instanceof InstrumentationConfiguration)) return false;
188 
189     InstrumentationConfiguration that = (InstrumentationConfiguration) o;
190 
191     if (!classNameTranslations.equals(that.classNameTranslations)) return false;
192     if (!classesToNotAcquire.equals(that.classesToNotAcquire)) return false;
193     if (!instrumentedPackages.equals(that.instrumentedPackages)) return false;
194     if (!instrumentedClasses.equals(that.instrumentedClasses)) return false;
195     if (!interceptedMethods.equals(that.interceptedMethods)) return false;
196 
197 
198     return true;
199   }
200 
201   @Override
hashCode()202   public int hashCode() {
203     if (cachedHashCode != 0) {
204       return cachedHashCode;
205     }
206 
207     int result = instrumentedPackages.hashCode();
208     result = 31 * result + instrumentedClasses.hashCode();
209     result = 31 * result + classNameTranslations.hashCode();
210     result = 31 * result + interceptedMethods.hashCode();
211     result = 31 * result + classesToNotAcquire.hashCode();
212     cachedHashCode = result;
213     return result;
214   }
215 
remapParamType(String desc)216   public String remapParamType(String desc) {
217     return typeMapper.remapParamType(desc);
218   }
219 
remapParams(String desc)220   public String remapParams(String desc) {
221     return typeMapper.remapParams(desc);
222   }
223 
mappedTypeName(String internalName)224   public String mappedTypeName(String internalName) {
225     return typeMapper.mappedTypeName(internalName);
226   }
227 
shouldIntercept(MethodInsnNode targetMethod)228   boolean shouldIntercept(MethodInsnNode targetMethod) {
229     if (targetMethod.name.equals("<init>")) {
230       return false; // sorry, can't strip out calls to super() in constructor
231     }
232     return methodsToIntercept.contains(new MethodRef(targetMethod.owner, targetMethod.name))
233         || methodsToIntercept.contains(new MethodRef(targetMethod.owner, "*"));
234   }
235 
convertToSlashes(Set<MethodRef> methodRefs)236   private static Set<MethodRef> convertToSlashes(Set<MethodRef> methodRefs) {
237     HashSet<MethodRef> transformed = new HashSet<>();
238     for (MethodRef methodRef : methodRefs) {
239       transformed.add(new MethodRef(internalize(methodRef.className), methodRef.methodName));
240     }
241     return transformed;
242   }
243 
internalize(String className)244   private static String internalize(String className) {
245     return className.replace('.', '/');
246   }
247 
248   public static final class Builder {
249     public final Collection<String> instrumentedPackages = new HashSet<>();
250     public final Collection<MethodRef> interceptedMethods = new HashSet<>();
251     public final Map<String, String> classNameTranslations = new HashMap<>();
252     public final Collection<String> classesToNotAcquire = new HashSet<>();
253     public final Collection<String> packagesToNotAcquire = new HashSet<>();
254     public final Collection<String> instrumentedClasses = new HashSet<>();
255     public final Collection<String> classesToNotInstrument = new HashSet<>();
256     public final Collection<String> packagesToNotInstrument = new HashSet<>();
257     public String classesToNotInstrumentRegex;
258 
259 
Builder()260     public Builder() {
261     }
262 
Builder(InstrumentationConfiguration classLoaderConfig)263     public Builder(InstrumentationConfiguration classLoaderConfig) {
264       instrumentedPackages.addAll(classLoaderConfig.instrumentedPackages);
265       interceptedMethods.addAll(classLoaderConfig.interceptedMethods);
266       classNameTranslations.putAll(classLoaderConfig.classNameTranslations);
267       classesToNotAcquire.addAll(classLoaderConfig.classesToNotAcquire);
268       packagesToNotAcquire.addAll(classLoaderConfig.packagesToNotAcquire);
269       instrumentedClasses.addAll(classLoaderConfig.instrumentedClasses);
270       classesToNotInstrument.addAll(classLoaderConfig.classesToNotInstrument);
271       packagesToNotInstrument.addAll(classLoaderConfig.packagesToNotInstrument);
272       classesToNotInstrumentRegex = classLoaderConfig.classesToNotInstrumentRegex;
273     }
274 
doNotAcquireClass(Class<?> clazz)275     public Builder doNotAcquireClass(Class<?> clazz) {
276       doNotAcquireClass(clazz.getName());
277       return this;
278     }
279 
doNotAcquireClass(String className)280     public Builder doNotAcquireClass(String className) {
281       this.classesToNotAcquire.add(className);
282       return this;
283     }
284 
doNotAcquirePackage(String packageName)285     public Builder doNotAcquirePackage(String packageName) {
286       this.packagesToNotAcquire.add(packageName);
287       return this;
288     }
289 
addClassNameTranslation(String fromName, String toName)290     public Builder addClassNameTranslation(String fromName, String toName) {
291       classNameTranslations.put(fromName, toName);
292       return this;
293     }
294 
addInterceptedMethod(MethodRef methodReference)295     public Builder addInterceptedMethod(MethodRef methodReference) {
296       interceptedMethods.add(methodReference);
297       return this;
298     }
299 
addInstrumentedClass(String name)300     public Builder addInstrumentedClass(String name) {
301       instrumentedClasses.add(name);
302       return this;
303     }
304 
addInstrumentedPackage(String packageName)305     public Builder addInstrumentedPackage(String packageName) {
306       instrumentedPackages.add(packageName);
307       return this;
308     }
309 
doNotInstrumentClass(String className)310     public Builder doNotInstrumentClass(String className) {
311       this.classesToNotInstrument.add(className);
312       return this;
313     }
314 
doNotInstrumentPackage(String packageName)315     public Builder doNotInstrumentPackage(String packageName) {
316       this.packagesToNotInstrument.add(packageName);
317       return this;
318     }
319 
setDoNotInstrumentClassRegex(String classNameRegex)320     public Builder setDoNotInstrumentClassRegex(String classNameRegex) {
321       this.classesToNotInstrumentRegex = classNameRegex;
322       return this;
323     }
324 
325     @SuppressWarnings("AndroidJdkLibsChecker")
build()326     public InstrumentationConfiguration build() {
327       // Remove redundant packages, e.g. remove 'android.os' if 'android.' is present.
328       List<String> minimalPackages = new ArrayList<>(instrumentedPackages);
329       if (!instrumentedPackages.isEmpty()) {
330         Collections.sort(minimalPackages);
331         Iterator<String> iterator = minimalPackages.iterator();
332         String cur = iterator.next();
333         while (iterator.hasNext()) {
334           String element = iterator.next();
335           if (element.startsWith(cur)) {
336             iterator.remove();
337           } else {
338             cur = element;
339           }
340         }
341       }
342       // Remove redundant classes that are already specified by a package. We do this to avoid
343       // unnecessarily creating sandboxes if a class is specified to be instrumented via
344       // '@Config(shadows=...)'.
345       List<String> minimalClasses =
346           instrumentedClasses.stream()
347               .filter(className -> minimalPackages.stream().noneMatch(className::startsWith))
348               .collect(Collectors.toList());
349 
350       return new InstrumentationConfiguration(
351           classNameTranslations,
352           interceptedMethods,
353           minimalPackages,
354           minimalClasses,
355           classesToNotAcquire,
356           packagesToNotAcquire,
357           classesToNotInstrument,
358           packagesToNotInstrument,
359           classesToNotInstrumentRegex);
360     }
361   }
362 }
363